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.