package main: The Declaration That Makes Code executable
Every language has a notion of where a program starts. Go's is a type declaration: the main package. A package is a directory of Go files sharing one name, and exactly one of those names — main — tells the toolchain "this directory builds an executable, not a library." Inside it, func main() is the function the operating system calls first. Nothing invokes main for you implicitly; its existence is the whole contract.
Start clean:
mkdir hello && cd hello
go mod init example.com/hello
go mod init writes a two-line go.mod declaring the module path. Without it, go run . refuses with go.mod file not found in current directory or any parent directory, because directory-based commands resolve packages through the module. (A single named file that imports only the standard library is the exception: go run hello.go works standalone — until you need a second file, at which point the module earns its keep.)
Now the program:
package main
import "fmt"
func main() {
boilingC := 100.0
boilingF := boilingC*9.0/5.0 + 32.0
fmt.Printf("water boils at %.1f°C = %.1f°F\n", boilingC, boilingF)
}
Read it as four decisions. package main marks this as a program. import "fmt" pulls in the formatting package so the code can talk to the terminal — Go loads packages by import path, and fmt lives in the standard library that shipped inside your toolchain. func main() is the entry point, with braces that open on the same line because Go's automatic semicolon insertion ends a line at } and other closing tokens, making your brace style a requirement rather than a preference. The body converts Celsius to Fahrenheit using ordinary arithmetic and prints with fmt.Printf, whose %.1f verbs format a float to one decimal place.
Run it:
go run .
# water boils at 100.0°C = 212.0°F
go run compiles the package and executes the result; nothing is written to your directory. If the code instead said package hello, the compiler would happily build a library — and go run would stop with go run: cannot run non-main package. The toolchain does not need to be told twice: main means program, anything else means code that other programs import. That distinction is the first half of Packages and Exported Identifiers, which covers what libraries look like and how names cross package boundaries. The declarations you just used — boilingC := 100.0 and the constant 100.0 — get their full treatment in Variables and Constants; for now they behave exactly as you would guess.
Borrowed or Kept: go run Versus go build
Your first program has two ways to execute, and the difference is entirely about the artifact left behind.
go run . # compile, execute, discard — directory unchanged
go build # compile, write ./hello (hello.exe on Windows), nothing runs
go run builds the binary into a temporary location and launches it, so your working directory stays clean. It is the edit-compile-run loop: change a line, run again, see output. go build is the production shape of the same act — it writes an executable named after the module directory's final element (a module path like example.com/cmd/server produces server; a plain hello directory produces hello), with .exe appended when the target is Windows. The -o flag overrides the name:
go build -o bin/hello . # explicit path, the shape build scripts want
./bin/hello # no compiler needed to run it
That resulting file is the deployment story. It contains your compiled code and the standard library it uses — no runtime to install, no version to match, no dependency manifest to reconcile on the server. Copy it to a machine of the same OS and architecture and it runs. For a different platform, set GOOS/GOARCH as shown in Installing the Go Toolchain and build again.
Two behaviors of go build are worth internalizing now, because they explain surprises later:
- Building a
mainpackage writes the executable. Building a library package compiles it to verify it is valid, then discards the object — the output lives in the build cache, and no file appears in your directory. Silence means success. - The command you use decides which files get compiled.
go build .compiles every non-test.gofile in the directory as one package.go build hello.gocompiles exactly that file — so a helper defined inother.govanishes from the build and the compiler reportsundefined: helper.
That last one is the most common first-week confusion: the code exists, the error says otherwise, and the truth is that file-list mode takes your file list literally. Prefer directory and package patterns (go run ., go build ./...) so the package is the unit of compilation, which is also how tests and CI will invoke it.
A quick side-by-side to fix the roles in memory:
| Command | Compiles | Artifact | Use it when |
|---|---|---|---|
go run . |
yes, into temp storage | none in your directory | iterating on behavior |
go build |
yes | binary named after the directory | you need the program itself |
go build ./... |
yes | binaries for main packages, objects for libraries |
checking the whole tree compiles |
Once a build succeeds reliably, the next question is what the compiler silently refuses to accept — which is where Go's strictness starts teaching you.
The Compiler Refuses to Guess
Write a slightly bigger program and the first wall you hit will not be logic. It will be an error about something you considered harmless:
package main
import (
"fmt"
"strings"
)
func main() {
name := "gopher"
fmt.Println(strings.ToUpper(name))
}
Remove the strings call and the compiler answers:
./main.go:4:2: "strings" imported and not used
Declare a variable and never read it:
./main.go:7:6: count declared and not used
Both are compile errors in Go, not warnings you can ignore. This is deliberate. An unused import pulls a dependency into the binary's import graph for no reason — build time and artifact size spent on code that never runs — while an unused local variable usually means a missing line of logic above it, and naming it early is cheaper than debugging a symptom three functions away. Languages that downgrade these to warnings accumulate dead imports and orphaned variables until no one can tell which code is load-bearing. Go makes cleanup part of the build, so the code that compiles is the code that runs.
The mirror image of the rule: if you call fmt.Println without importing fmt, the compiler says undefined: fmt. It never searches for you. Imports are explicit, one line per package, and the resolution is exactly the name you wrote.
When an error appears, it arrives as file:line:column, and the column points at the offending token, not the line's start. The convention of reading errors bottom-up — the last line first, the original location after — applies here as everywhere. You will also notice errors from the semicolon rule if you write this:
func main()
{
fmt.Println("hi")
}
Go inserts a semicolon after func main() because a newline follows an identifier-and-parenthesis sequence, and a lone { cannot follow it. The language's answer to "where do braces go" is that the opening brace belongs on the line of the declaration — the compiler enforces it before you ever argue about style in a code review.
Notice what has not complained yet: the code above is misformatted, comments are absent, and naming could improve. Formatting and static analysis are separate tools with separate jobs — Formatting and Inspection Tools covers gofmt, go doc, and go vet — and comments deserve their own treatment in Comments and Doc Comments. The compiler's contract is narrower and absolute: the program is well-formed, every name resolves, and nothing declared is dead. Hold that contract as the baseline, and every later tool feels like added power rather than duplicated noise.
One Directory, One Package: Where Files Belong
The program above fits in one file. Real programs do not, and the first structural fact to absorb is that a package is a directory, not a file. Every .go file in the directory starts with the same package clause and contributes declarations to one namespace:
hello/
├── go.mod
├── main.go package main
└── report.go package main — same clause, same package
Add a function to report.go:
package main
import "fmt"
func report(label string, celsius float64) {
f := celsius*9.0/5.0 + 32.0
fmt.Printf("%s: %.1f°C / %.1f°F\n", label, celsius, f)
}
and call it from main.go with no import of anything — same-package declarations are visible to each other without ceremony, because they are one unit of compilation. Run go run . and both files compile together. Run go run main.go and you get undefined: report: file-list mode compiles only the files you named, which is occasionally useful for a quick experiment and almost always wrong otherwise.
The directory boundary is also the visibility boundary. Declarations in another package are unreachable unless their names begin with an uppercase letter — exported names — and even then require an import of that package's path. fmt exposes Println; it does not expose whatever internal helper computed it. That rule is the entire mechanism behind Go's small, opinionated public APIs, and Packages and Exported Identifiers works through how a directory becomes an importable unit, how names are chosen, and how packages stay acyclic.
Two practical consequences for a first project:
- Tests live beside the code:
main_test.goin the same directory, same package clause, compiled only bygo test. You will meet the pattern in Unit Tests with the testing Package. - A subdirectory is a new package.
hello/internal/configwithpackage configis imported by path, not by proximity, and its names are shared only through exports.
So the working loop for anything larger than a single file is: keep one package per directory, compile with patterns (go run ., go build ./..., go test ./...) instead of file lists, and let the package — never the individual file — be the thing you reason about. With entry point, commands, compiler rules, and layout settled, you have everything the syntax needs; comments are what make the next reader's experience match yours.
Quiz
1. You run `go run .` and then `go build` in the same directory. What is on disk afterward that was not there before?
2. A file begins with `package report` instead of `package main`, and you try `go run .`. What does the go command do?
3. You delete the only call to package strings but keep `import "strings"`. What happens, and why is it a compile error rather than a warning?
4. Your directory has main.go (which calls helper()) and other.go (which defines helper()). `go run main.go` reports `undefined: helper`, but `go run .` works. Why?
Flashcards
Question
What do `package main` and `func main()` each contribute to an executable?
Click to reveal answer
Answer
package main tells the toolchain the directory builds a program rather than a library, and provides the package the runtime calls into; func main() is the function that runs first inside it. If the package clause is anything else, go run refuses with 'cannot run non-main package' — no other declaration can serve as an entry point.
Question
What is the difference between `go run .` and `go build` in the same directory?
Click to reveal answer
Answer
go run compiles the package into temporary storage and executes it, leaving nothing in your working directory — the iteration loop. go build writes a persistent executable named after the directory's final element (with .exe for Windows targets) and does not run it — the deployment step. For library packages, go build compiles to verify and discards the object.
Question
Why does Go fail the build on an unused import or an unused local variable?
Click to reveal answer
Answer
Both usually indicate leftover or missing code: an unused import drags a dependency through the import graph for nothing, and an unused variable usually means a line of logic never got written. Making them errors keeps the compiled code equal to the written code — unlike languages where they are warnings that rot silently.
Question
How does go build name the executable it writes?
Click to reveal answer
Answer
For a main package it uses the last non-version element of the package's import path — building example.com/cmd/server writes server — with .exe appended for Windows targets, and -o overrides the name entirely. When compiling a list of .go files instead of a package, the binary is named after the first file in the list.
Question
`go run .` works, but `go run main.go` reports undefined for a function in another file of the same directory. Explain.
Click to reveal answer
Answer
The directory form compiles every non-test .go file in the package together, while the file-list form builds an ad hoc package containing only the files you named. Same-package declarations need no import — they need to be part of the compilation, which is why package patterns (., ./...) are the correct default.
Revision Notes
Key Takeaways
- 1. Executability is a package-level property: package main plus func main() is the entire entry-point contract
- 2. go run is the borrowed binary for iteration; go build is the kept binary for shipping — same compile, different artifact
- 3. Unused imports and unused locals are errors, not warnings, so the code that builds is the code that runs
- 4. One directory is one package: files share declarations freely, subdirectories are new packages, and uppercase marks exports
- 5. Compile with package patterns, not file lists, or the files you forgot will report as undefined
Interview Tips
- • "What's the difference between a main package and a library package in Go?" — main builds an executable with func main as entry point; any other package compiles into an object other code imports, and its API is exactly its exported (uppercase) names
- • "Why does Go error on an unused variable?" — give the reasoning, not the rule: it catches missing logic at compile time and keeps dependency graphs honest; follow with the fact that package-level unused variables are allowed because libraries expose state
- • "How would you ship this to a server?" — go build (optionally GOOS/GOARCH for the target), copy the single static binary; mention there is no runtime to install on the destination
- • "go build ./... vs go build" — ./... walks every package in the module subtree, building mains and type-checking libraries; plain go build handles only the current directory
Cheat Sheet
Your First Go Program
Commands:
| Command | Compiles | Leaves behind | Use for |
|---|---|---|---|
go run . |
yes (temp) | nothing | iterating on behavior |
go build |
yes | executable named after the dir (.exe on Windows) |
producing the program |
go build -o path . |
yes | exactly the path you named | build scripts, CI |
go build ./... |
yes | binaries for main pkgs; objects cached for libraries | checking the tree compiles |
go mod init <path> |
— | go.mod | first command in a new directory |
Program skeleton:
package main
import "fmt"
func main() {
fmt.Println("first program")
}
Rules the compiler enforces before logic runs:
- Only package main builds an executable (
go run: cannot run non-main packageotherwise) - Every referenced name must be imported:
undefined: fmt - No unused imports:
"strings" imported and not used - No unused locals:
count declared and not used - Opening brace stays on the declaration line (automatic semicolon insertion)
Decision rule: directory patterns (././...) compile the package; file lists compile only what you name — prefer patterns.
Continue Learning
Before You Start
- No prerequisites listed.