Day 7 · Concept 18
Modules & packages
A module is a versioned bundle of Go packages with a go.mod file at
its root. A package is a directory of .go files sharing the same
package foo declaration. The 2024 standard project layout is
small, opinionated, and learnable in 10 minutes.
1 · The intuition
Three concepts, one hierarchy. Module = the unit of versioning
and distribution (one go.mod at its root). Package = a
directory's worth of related code (one package foo declaration).
File = a single .go file inside a package.
Imports use the module path: import "github.com/me/app/internal/store".
A directory is a package; you can't split a package across multiple
directories or have two packages in one directory.
2 · Try it — create a module
# Create a directory; name doesn't need to match the module path
$ mkdir myapp && cd myapp
# Initialize a module — the path tells Go where to fetch this from
$ go mod init github.com/me/myapp
# This creates go.mod:
# module github.com/me/myapp
# go 1.22
# Add a dependency
$ go get github.com/google/uuid
# Tidy up — fetch missing, remove unused
$ go mod tidy
# Now your go.mod has:
# require github.com/google/uuid v1.6.0
# And go.sum has the cryptographic hashes3 · The 2024 standard project layout
myapp/
├── go.mod # module declaration
├── go.sum # dependency lock file
├── README.md
├── Makefile # optional, common
├── cmd/ # entry points — one dir per binary
│ ├── server/
│ │ └── main.go # the actual server binary
│ └── cli/
│ └── main.go # an admin CLI
├── internal/ # private to this module
│ ├── store/
│ │ ├── store.go
│ │ └── store_test.go
│ └── handler/
│ └── handler.go
├── pkg/ # public API (if you publish a library)
│ └── api/
│ └── types.go
└── api/ # OpenAPI spec, protobuf — not Go
└── openapi.yamlThe internal/ rule. Anything under
internal/ can
only be imported by code in the same module. This is the compiler-enforced
"private API" boundary. Use it heavily — it lets you refactor without breaking
external callers.4 · The go.mod file in detail
module github.com/me/myapp
go 1.22
require (
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.9.0
go.etcd.io/etcd/client/v3 v3.5.12
)
require (
// indirect — pulled in transitively
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
)
// Override a dependency to a fork
replace github.com/old/lib => github.com/me/lib v1.2.3
// Exclude a buggy version
exclude github.com/bad/dep v1.0.05 · Semantic versioning & imports
| Version pattern | Meaning |
|---|---|
v1.x.y | Stable; backward-compatible |
v0.x.y | Pre-stable; anything can break |
v2.x.y+ | Path must include /v2: github.com/me/lib/v2 |
| commit hash | "Pseudo-version" — v0.0.0-20240509... |
@latest | go get pkg@latest — fetch newest tag |
6 · Visibility rules
- Capitalised = exported.
User,Authenticate,MaxRetries— visible outside the package. - Lowercase = package-private.
user,authenticate— only visible within the same package. internal/directory = module-private. Importable only from within the same module tree.vendor/directory = vendored deps.go mod vendorcreates it;-mod=vendoruses it (default if it exists).
7 · Day-to-day commands
| Command | What it does |
|---|---|
go mod init <path> | Create a new module |
go mod tidy | Add missing deps, remove unused |
go get pkg@v1.2.3 | Add or upgrade a specific dep |
go get pkg@latest | Add or upgrade to latest tag |
go get -u ./... | Upgrade all deps to latest minor/patch |
go mod download | Pre-fetch into module cache |
go mod vendor | Create vendor/ with all deps |
go list -m all | List all modules + versions in scope |
go mod why pkg | Show why a module is needed |
go mod graph | Print the full dep graph |
8 · Common mistakes
- Putting binaries at the root. Use
cmd/<name>/main.goper binary. Lets the same module ship multiple binaries. - Skipping
internal/. Without it, every type becomes part of your public API. Once you've shipped a name as public, refactoring becomes painful. - Massive packages. A package with 50+ files is hard to navigate. Split by concern; package names should be a single noun.
- Circular imports. Go forbids them. The fix: extract the shared types to a third package both can import.
- Manual editing
go.mod. Always usego get/go mod tidy— they handlego.sumtoo. - Forgetting to upgrade the major version path.
github.com/me/lib v2.0.0needs the import path to include/v2. Otherwise the module system treats it as a different module.
9 · Exercises (~15 min)
- Build a two-binary module. Create
myapp/cmd/server/main.goandmyapp/cmd/cli/main.gothat share code frommyapp/internal/core/.go build ./cmd/.... - Add a dependency.
go get github.com/google/uuid, use it, rungo mod tidy. Watchgo.modandgo.sumupdate. - Test the internal/ boundary. Try importing from
internal/from a sibling module. Note the compile error. - Vendor it.
go mod vendor. Rungo build— Go usesvendor/. Check it in to a separate branch.
10 · When it clicks
- You start every new project with
go mod initand thecmd/internal/pkglayout. - You put implementation in
internal/by reflex — only deliberate exports go inpkg/. - You run
go mod tidybefore every commit. - You read
go.moddiffs in code review and spot risky upgrades.
Found this useful?