Go
Packages & Modules
Every Go file you've seen in this domain started with package main —
Introduction to Go mentioned it without explaining
it. This chapter is the full picture: how Go organizes code into packages, and how modules manage
dependencies between them, a structurally different approach from
C's #include-based system.
- What it is
- A package is Go's unit of code organization — every file in a directory belongs to one. A module is a versioned collection of packages, Go's unit of dependency management.
- Mental model
- Where C's
#includetextually pastes a header's contents into your file, Go'simportbrings in a real, separately-compiled package by name — no text substitution involved at all. - Key numbers
- One
go.modfile per module, at the project root, naming the module and pinning every dependency's exact version. - Reach for it when
- Organizing any Go project beyond a single file — which is to say, nearly every real Go project.
Packages
// file: mathutils/mathutils.go
package mathutils
func Square(n int) int { // capitalized — EXPORTED, visible outside this package
return n * n
}
func square2(n int) int { // lowercase — unexported, only visible inside mathutils
return n * n
}
Every .go file in the same directory belongs to the same package — there's no per-file
boundary the way each .c file is its own translation unit in
C's compilation model. Capitalization is, again, the
entire access control mechanism, exactly as in struct
fields.
import — not #include
import (
"fmt" // standard library package
"myproject/mathutils" // a package within your own module
)
func main() {
fmt.Println(mathutils.Square(5)) // 25
}
import is not text substitution — unlike C's
#include, which pastes a header's literal contents into your file before real
compilation starts, Go's import brings in an already-compiled, real package the compiler
understands directly. There are no header files, no forward declarations, and no risk of the double-inclusion
problem header guards exist to solve —
Go's package system doesn't have that category of bug at all.
Modules and go.mod
go mod init myproject # creates go.mod, declaring this directory as a module
go get github.com/some/dependency # adds a dependency, recorded in go.mod with an exact version
// go.mod
module myproject
go 1.21
require github.com/some/dependency v1.2.3
A module is a versioned collection of one or more packages — go.mod names the module and
pins every dependency to an exact version, so a build is reproducible across machines. This solves a
real, historically painful problem: C has no standard, built-in equivalent, and dependency management in
C/C++ projects is often handled ad hoc, per-project (a git submodule, a vendored copy, a system package
manager).
Real-world applications
- Every real Go project — a
go.modat the root is close to universal in any Go codebase beyond a single-file script. - Internal packages — splitting a large service into packages by responsibility (
handlers,database,config) is standard Go project structure. - Reusable libraries — a well-scoped, exported package API (capitalized names only for what's genuinely meant to be used externally) is how Go libraries are shared on sites like pkg.go.dev.
Gotchas
- Go's package name and its import path don't have to match exactly, which occasionally confuses newcomers — the import path is the directory/module path, while the package name is whatever's declared with
packageat the top of the file. - Circular imports (package A imports package B, which imports package A) are a compile error in Go — unlike C, where circular
#includes are technically possible to work around with guards, Go simply disallows the cycle outright. - An unused import is a compile error (see Introduction to Go's gotchas) — the same strictness applied here as with unused variables.
- Committing a
go.sumfile alongsidego.modis standard practice — it records cryptographic checksums of every dependency, protecting against a dependency silently changing content at the same version number.