18 / 20 · Day 7
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

shell terminal
# 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 hashes

3 · The 2024 standard project layout

shell myapp/
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.yaml
The 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

go go.mod · a real module
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.0

5 · Semantic versioning & imports

Version patternMeaning
v1.x.yStable; backward-compatible
v0.x.yPre-stable; anything can break
v2.x.y+Path must include /v2: github.com/me/lib/v2
commit hash"Pseudo-version" — v0.0.0-20240509...
@latestgo 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 vendor creates it; -mod=vendor uses it (default if it exists).

7 · Day-to-day commands

CommandWhat it does
go mod init <path>Create a new module
go mod tidyAdd missing deps, remove unused
go get pkg@v1.2.3Add or upgrade a specific dep
go get pkg@latestAdd or upgrade to latest tag
go get -u ./...Upgrade all deps to latest minor/patch
go mod downloadPre-fetch into module cache
go mod vendorCreate vendor/ with all deps
go list -m allList all modules + versions in scope
go mod why pkgShow why a module is needed
go mod graphPrint the full dep graph

8 · Common mistakes

  • Putting binaries at the root. Use cmd/<name>/main.go per 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 use go get / go mod tidy — they handle go.sum too.
  • Forgetting to upgrade the major version path. github.com/me/lib v2.0.0 needs the import path to include /v2. Otherwise the module system treats it as a different module.

9 · Exercises (~15 min)

  1. Build a two-binary module. Create myapp/cmd/server/main.go and myapp/cmd/cli/main.go that share code from myapp/internal/core/. go build ./cmd/....
  2. Add a dependency. go get github.com/google/uuid, use it, run go mod tidy. Watch go.mod and go.sum update.
  3. Test the internal/ boundary. Try importing from internal/ from a sibling module. Note the compile error.
  4. Vendor it. go mod vendor. Run go build — Go uses vendor/. Check it in to a separate branch.

10 · When it clicks

  • You start every new project with go mod init and the cmd/internal/pkg layout.
  • You put implementation in internal/ by reflex — only deliberate exports go in pkg/.
  • You run go mod tidy before every commit.
  • You read go.mod diffs in code review and spot risky upgrades.
Found this useful?