Skip to content
academia.sh

Lesson 02 / 18

Environment Setup and the First Program

Three things are mandatory for a Go source to compile: a package declaration, an entry point (func main), and a module file; when one of the three is missing, compilation is rejected even if the rest of the source has not changed at all — the module file never sits inside the source itself.

Published

In this lesson

  • Removes each of the three mandatory parts a source needs to run and classifies the compile result one by one
  • Runs proof that two files in the same package see each other without writing an import
  • Shows that the module file stands outside the source by removing it while the source stays unchanged
Contents

The previous lesson measured four rejection reasons; all of them arose when something extra was written into the source or a promise went unkept. This lesson asks the reverse: what is the smallest set of things that must be written for a source to compile and run at all? A Go program asks for three things at once — a package declaration, an entry point, a module file — and when one of them is missing, compilation stops no matter how correctly the rest of the source is written.

The How Computers Work course (M01/K01) built, as a concept, how a program is started by the operating system, the notion of an entry point, and how a process reaches its first instruction. That concept is not rebuilt here — one sentence is enough to state the difference: there, it was explained that an entry point has to exist; here we measure which signature and at what point in compilation Go enforces that requirement.

Three Mandatory Parts

The three each sit in a different place. The package declaration (package main) is the first line of the source and states which package it belongs to; for an executable program, this package name has to be main. The entry point is the func main() function defined inside the main package, and it is the first Go instruction called directly by the operating system, not by a virtual machine. The module file, on the other hand, sits outside the source — a separate text file living in the same directory as the source, go.mod, and it is the only thing that tells the compiler “this directory is the root of a module”.

All three being mandatory is not a coincidence — it is the answer to three separate questions. The package declaration answers “what is this source part of” and states that not even a single source file can stand without a package — in Go there is no such thing as a packageless source, even the smallest program has to carry a package name. The entry point answers “does execution start here” — a package can hold more than one function, but there is exactly one signature that tells the operating system which function to start from. The module file answers “where does this package sit” and carries information independent of the package itself — the same package name can repeat across many different directories, but which module a directory belongs to is read only from the go.mod file in that directory (or one above it).

Since all three questions look “mandatory,” it can be tempting to check them together as a single checklist — one check asking “are all three complete.” This lesson does the opposite: it removes each one one at a time and reads the result separately, because the same not compiled answer can hide three separate reasons, and knowing at which stage each of the three arises is the first step to understanding why a source does not compile.

We are going to remove these three parts one at a time and measure the compile result. The measurement tool is very close to the previous lesson’s compiles function; the only difference is that it also takes, as a separate parameter, whether the module file will be written.

// build.go — helper that compiles while checking each part
package main

import (
	"os"
	"os/exec"
	"path/filepath"
)

func compilesWithParts(source string, writeMod bool) bool {
	dir, err := os.MkdirTemp("", "trial")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dir)
	if writeMod {
		os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	}
	os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

func classify(ok bool) string {
	if ok {
		return "compiled"
	}
	return "not compiled"
}

LB4. The measurement tool opens one parameter further than the previous lesson: writeMod decides whether the module file is written for that run. The source itself is never affected by this parameter; keeping that in mind is necessary to correctly read what the fourth line below measures.

This extension looks small but carries a design decision: in the previous lesson, where the unwritten thing sat was always inside the source — a line either existed or it did not. In this lesson, for the first time, where the unwritten thing sits moves outside the source, and the measurement tool has to represent that with a separate argument. The next section shows why this distinction matters.

We run four cases in order: with nothing missing, with no package declaration, with no entry point, with no module file.

// parts.go — each of the three mandatory parts is removed one at a time
package main

import "fmt"

const completeSource = `package main

func main() {}
`
const noPackageSource = `func main() {}
`
const noMainSource = `package main
`

func main() {
	fmt.Printf("%-30s %s\n", "missing part", "result")
	fmt.Printf("%-30s %s\n", "nothing missing", classify(compilesWithParts(completeSource, true)))
	fmt.Printf("%-30s %s\n", "no package declaration", classify(compilesWithParts(noPackageSource, true)))
	fmt.Printf("%-30s %s\n", "no entry point (main)", classify(compilesWithParts(noMainSource, true)))
	fmt.Printf("%-30s %s\n", "no module file", classify(compilesWithParts(completeSource, false)))
}
missing part                   result
nothing missing                compiled
no package declaration         not compiled
no entry point (main)          not compiled
no module file                 not compiled

The first line is the control group: with nothing missing, the source compiles. Each of the next three lines removes exactly one part from the control group, and the result changes in the same direction each time — all three stop compilation. But the three do not stop it for the same reason. With no package declaration, the compiler has no idea at all which package the source belongs to; this is the earliest branch to be cut, because a package name is expected on the source’s very first line. With no entry point, the source compiles as a valid package, but no executable is produced — there is no error in the source, it is only missing an executable end; go build ./... counts this too as not compiled, because an entry point is a mandatory contract for the main package. The fourth line is the most indirect and leads into this lesson’s limiting measurement.

Looking at the noPackageSource constant, exactly one line is missing: package main. When that line is removed, what remains — func main() {} — is not meaningless on its own: it is a function definition and its syntax is correct. The compiler’s rejection here is not about syntax but about context: every Go file is read as part of a package, and without a package name it cannot place a file into any package at all. This is a different tone from the four rejections in the previous lesson: those four sources violated syntax rules, whereas this source is a syntactically complete function body that is nameless.

The first two rejections belong to a family close to the four rejections the previous lesson measured: both are something left missing in the source’s own syntax, and the compiler sees this while reading the source. The absence of the entry point is caught at a different stage. go build does two things one after the other: it first compiles the source into a package, then it links an executable from that package. Package compilation accepts main.go as a valid Go file; the linking stage looks for the main package to carry a func main() function and stops when it cannot find one. This distinction matters because the same source could compile without any problem in a different context — for instance as a library package imported by another program; the entry-point requirement applies only to the main package, only when an executable is being requested.

Two Files, One Package

Do the three mandatory parts have to sit in a single file? No. Two files in the same directory, as long as they carry the same package declaration, count as belonging to one package and are compiled together — even if one of them does not carry a func main. The build.go file above and every other file used in this lesson were already showing this; here we build the same pattern in its plainest form.

The practical importance of this shows up as a program grows. If everything had to be written into a single file carrying func main(), a program’s length would be limited by what a single file can stay readable at. Because Go defines a package by its declaration and not by file count, a package’s responsibilities can be spread across more than one file — and this split never changes the compile result: build.go, helper.go, and together.go would compile just as well as three separate files or merged into one, and would produce the same binary. The decision to split exists purely for the convenience of the reader and the writer.

// helper.go — second file, belongs to the same package
package main

import "fmt"

func greet(name string) string {
	return fmt.Sprintf("hello, %s", name)
}
// together.go — two separate files, one package, compiled together
package main

import "fmt"

func main() {
	fmt.Println(greet("package"))
}
hello, package

LB5. together.go calls the greet function defined in helper.go without writing any import at all. What is measured in this lesson is not the mere existence of a second file — we were already using one before reaching this lesson — it is that two separate files compile into a single binary: go run . reads every .go file in the directory, never looks for an import between them, and checks only that the package name matches.

This behavior asks us to re-read the “nothing missing” line from the earlier measurement too. The single-file completeSource used in that line was written and compiled on its own, outside build.go — classify and compilesWithParts were not being called there, the source only carried package main and an empty func main. Here, on the other hand, together.go depends on helper.go in the same directory, and that dependency needs no import crossing a file boundary at all. The two measurements complement each other: one shows the smallest possible shape of a package, the other shows the shape of a package spread across more than one file.

This also explains why a package declaration is written per file. The compiler learns which files together form a whole not from the file name but from each file’s own first line. build.go, helper.go, and together.go all write main, and all three join the same package; if any one of them had written a different name, compilation would stop right there, even inside the same directory.

One consequence of this behavior is that imports work at two separate levels. together.go imports the fmt package on its own, because it uses a piece of the standard library; but it does not import greet, defined in helper.go, because that already belongs to the same package. The import line is only needed for names that cross a package boundary — inside the same package, a file boundary carries no meaning. The course’s Packages topic will flip this boundary the other way: there, files will still stay in the same package, but some names will not be visible outside the package at all.

Limiting Measurement: The Module File Is Not Part of the Source

Return to the fourth line. The source used in the no module file line is byte for byte identical to the one used in the first line — the completeSource constant passes through both calls unchanged. The only difference is that the writeMod argument given to compilesWithParts is false: no go.mod is ever written into the temporary directory.

LB6. What is measured in this line is not a source error but a missing context. The source’s own package declaration and entry point stand complete; the compiler still rejects it, because it will not agree to compile a directory without knowing which module it belongs to.

This line separates qualitatively from the lesson’s first two rejections. Removing the package declaration or the entry point meant touching the source; here the source was never even opened, only another file in the directory was deleted. A reader looking at the completeSource constant could not tell from the source itself why it sometimes compiles and sometimes does not — the answer sits outside the source, in the directory’s contents.

The practical consequence is this: copying a source file into another directory is useless if that directory has no module file — the source will not compile even if it was copied exactly as is. The module file is not part of the source the way the package declaration is; it is the source’s context. This distinction matters because, even though both are counted “mandatory,” they are mandatory for different things: the package declaration carries the source’s own identity, the module file states where the source sits. The course’s Packages topic will deepen this distinction; what is measured here is only the requirement itself — a source, however correctly written, never turns into a binary without a module file.

The measurement tool used in this lesson never changed the module file’s content — it only changed whether it existed. A module file, even in its smallest form asking for no outside dependency at all, is mandatory on its own; having no dependency line added to it is not a reason for the compiler to reject the module. What is mandatory is the module file’s existing, not how full its content is.

The module file being independent of the source also means the same source can be moved under more than one module. Changing directory and writing a fresh go.mod, without changing the source at all, makes it part of another module. Changing the package declaration, on the other hand, means changing the source itself. This difference between the two shows that the word “mandatory” does not carry a single meaning — something can be mandatory inside the source, or mandatory around the source, and the cost of removing each is not the same: one breaks the source, the other only invalidates the place where the source sits.

This threefold distinction — identity, contract, context — also gives a retrospective frame to the four rejections measured in the first lesson. The unused variable and the unused import were about the source’s own identity: every name written in the source should deserve to stay part of the source. The unwritten return value was a broken contract: the signature makes a promise, the body must keep it. The third category added in this lesson — context — never appeared in the first lesson, because the four sources there were already being written into a module. Seeing that mandatoriness can sit in three separate places will keep proving useful later in the course: a variable’s zero value will belong to its identity, a function’s named return will belong to its contract, and a package’s export rule will belong to its context. None of these three categories will be called by these names anywhere in this course — what is built here is only a reading habit: the first question on seeing a not compiled answer should not be “which line is missing” but “where does the missing thing sit”.

Summary

  • Three things are mandatory for a Go source to compile: a package declaration, an entry point (func main), a module file.
  • Compilation stops when any of the three is missing, but not for the same reason: one leaves the source’s identity incomplete, one its contract, one its context.
  • Two files in the same directory see each other and compile into a single binary without writing an import, as long as they carry the same package name.
  • The module file is the source’s context, not its part: the same source, unchanged, stops compiling once the module file is removed.
  • Mandatoriness does not carry one meaning — what is mandatory inside the source and what is mandatory around the source cost different things when removed.

Next Step

This lesson measured what is required for a source to run; nowhere did it ask what is or is not written into a value. func main() {} was an empty body and had no variable inside it at all. The next lesson fills that gap: do you have to write a variable’s type when defining it, where does short declaration infer the type from, and who fills the unwritten values in a sequential constant list?

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close