Modules and packaging
The Go module is the unit of versioned dependency management. A module is declared by a go.mod file at its root; the file specifies the module’s import path, the Go version it targets, and its dependencies (with explicit versions). The go.sum file records cryptographic hashes of dependency modules — admitting reproducible builds. The go command (introduced as the modular successor to the older GOPATH workspace model in Go 1.11) handles module operations: go mod init, go get, go mod tidy, go build, go test, go install. The combination — declarative go.mod, hash-locked go.sum, semver-based dependencies, the central proxy at proxy.golang.org — is the substance of Go’s package management.
A module
A module is a directory tree containing a go.mod file at its root:
myproject/
├── go.mod
├── go.sum
├── main.go
├── internal/
│ ├── auth/
│ │ └── auth.go
│ └── store/
│ └── store.go
└── pkg/
└── api/
└── api.go
// go.mod
module github.com/user/myproject
go 1.24
require (
github.com/some/dep v1.2.3
golang.org/x/sync v0.5.0
)
The module line declares the module’s import path; subpackages are imported as <module>/subpath:
import "github.com/user/myproject/pkg/api"
import "github.com/user/myproject/internal/auth"
Creating a module
mkdir myproject
cd myproject
go mod init github.com/user/myproject
The conventional module path is the URL where the source is hosted (github.com/user/..., gitlab.com/user/..., custom domains). The path admits Go’s tooling fetching the module directly from the source.
For local-only modules, any path admits initialisation:
go mod init example.com/myproject
go mod init example/myproject # also OK
go.mod
The principal directives:
module github.com/user/myproject
go 1.24 // language version
require (
github.com/foo/bar v1.2.3
golang.org/x/sync v0.5.0
example.com/dep v0.1.0-alpha.1
)
// Replace a dependency (typically for local development):
replace github.com/foo/bar => ../local-bar
// Replace at a specific version:
replace github.com/foo/bar v1.2.3 => github.com/forked/bar v1.2.3-fix1
// Exclude a specific version:
exclude github.com/buggy/lib v0.5.0
The replace directive is conventional for:
- Local development — substituting an in-progress local copy.
- Forks — using a forked version with bug fixes.
- Vendoring overrides — pointing to a known-good version.
go.sum
The go.sum file records cryptographic hashes of dependency modules:
github.com/foo/bar v1.2.3 h1:Abc123...
github.com/foo/bar v1.2.3/go.mod h1:Xyz789...
The mechanism admits reproducible builds — go build verifies that each downloaded dependency matches the recorded hash. If a malicious actor modifies a dependency, the hash mismatch is detected.
The go.sum is conventionally committed to version control alongside go.mod.
Adding dependencies
go get github.com/foo/bar # latest
go get github.com/foo/bar@v1.2.3 # specific version
go get github.com/foo/bar@latest # explicit latest
go get github.com/foo/bar@master # branch
go get github.com/foo/bar@abc1234 # specific commit
go get -u # update all
go get -u github.com/foo/bar # update one
The conventional workflow:
# Edit code to import the new package:
import "github.com/foo/bar"
# Then run:
go mod tidy
go mod tidy adds missing dependencies and removes unused ones; the conventional one-step “make go.mod consistent with the source”.
Removing dependencies
Remove the import from the source, then:
go mod tidy
The dependency is removed from go.mod and go.sum.
Versioning
Go modules use semantic versioning:
v1.2.3
│ │ │
│ │ └─ patch: bug fixes (backward compatible)
│ └─── minor: new features (backward compatible)
└───── major: breaking changes
The v prefix is required. Pre-1.0 versions (v0.x.y) admit breaking changes in any release.
For major versions ≥ 2, the major version becomes part of the import path:
github.com/foo/bar (v0.x.y or v1.x.y)
github.com/foo/bar/v2 (v2.x.y)
github.com/foo/bar/v3 (v3.x.y)
The mechanism admits two major versions of the same library coexisting in one program — useful during gradual migrations. The discipline is sometimes called semantic import versioning.
The module cache
go get downloads dependencies to a local cache:
$GOPATH/pkg/mod/ (module sources)
$GOPATH/pkg/mod/cache/download/ (download cache)
The cache is shared across projects. Dependencies are typically immutable — once downloaded, a specific module version is never re-downloaded.
To clear the cache:
go clean -modcache
The proxy
Module downloads conventionally route through a module proxy:
GOPROXY=https://proxy.golang.org,direct # default
The proxy:
- Caches modules — reduces dependency on the original source.
- Provides immutability — once downloaded, modules cannot disappear.
- Admits private modules — the
GOPRIVATEvariable bypasses the proxy for specific paths.
GOPRIVATE=*.corp.example.com,github.com/myorg/*
The conventional public proxy is proxy.golang.org, run by Google.
Internal packages
Subpackages under internal/ are visible only to packages whose import path is rooted at the parent of internal/:
github.com/user/myproject/
├── pkg/api/ (importable from anywhere)
└── internal/auth/ (importable only by myproject)
The mechanism admits exposing helpers without making them part of the public API.
Vendoring
The go mod vendor command produces a vendor/ directory containing the source of all dependencies:
go mod vendor # produces vendor/
go build -mod=vendor # build using vendor (default in vendored projects)
The mechanism admits:
- Reproducibility — the source is committed to the project.
- Offline builds — no network access required.
- Dependency review — visible in version control.
The conventional discipline:
- For libraries — do not vendor.
- For applications — vendor or rely on the proxy; either is acceptable.
Workspace mode (Go 1.18+)
The go.work file admits multi-module workspaces:
workspace/
├── go.work
├── module1/
│ ├── go.mod
│ └── ...
├── module2/
│ ├── go.mod
│ └── ...
// go.work
go 1.24
use (
./module1
./module2
)
The go.work admits developing multiple modules side-by-side without replace directives. The mechanism is conventional for monorepos with multiple Go modules.
go work init # create a workspace
go work use ./module1 # add a module
go work sync # sync go.work with go.mod files
The go.work is conventionally not committed to version control — it is local to the developer.
Common patterns
Standard project layout
myproject/
├── go.mod
├── go.sum
├── README.md
├── cmd/
│ └── myserver/
│ └── main.go (the entry point)
├── internal/
│ ├── auth/
│ ├── store/
│ └── api/
├── pkg/ (public, optional)
│ └── client/
└── tests/
└── integration/
The conventional structure:
cmd/— entry-point binaries; one subdirectory per binary.internal/— private packages.pkg/— public packages (use sparingly).- Module-root files (
go.mod,go.sum,README.md).
Adding a dependency
go get github.com/google/uuid
import "github.com/google/uuid"
func newID() string {
return uuid.NewString()
}
Pinning a version
// go.mod
require github.com/foo/bar v1.2.3
The go.mod records the exact version; subsequent go build uses precisely that version.
Updating dependencies
go get -u # update all to latest minor/patch
go get -u=patch # update to latest patch only
go get github.com/foo/bar@v1.3.0 # specific update
go mod tidy # clean up
Local development with replace
// go.mod (during development)
replace github.com/myorg/lib => ../lib
require github.com/myorg/lib v0.0.0
The replace admits using a local checkout instead of the published version.
The conventional discipline: remove the replace before pushing the consuming module.
Running tests across modules
go test ./... # all packages in the current module
go test ./internal/auth/... # subset
Building binaries
go build . # build the current package
go build -o myserver ./cmd/myserver # specific binary
go install ./cmd/myserver # install to $GOPATH/bin
# Cross-compilation:
GOOS=linux GOARCH=amd64 go build -o myserver-linux ./cmd/myserver
GOOS=darwin GOARCH=arm64 go build -o myserver-mac ./cmd/myserver
GOOS=windows GOARCH=amd64 go build -o myserver.exe ./cmd/myserver
Build tags
The //go:build directive admits per-build-target source files:
//go:build linux
package mypkg
func init() { /* linux-specific */ }
//go:build linux && amd64
func sysCall() { /* ... */ }
The _linux_amd64 filename suffix is an alternative form (without explicit directives):
config_linux.go
config_darwin.go
config_windows.go
Inspecting modules
go list -m all # all modules in the build
go list -m -versions github.com/foo/bar # available versions
go mod why github.com/foo/bar # why is this dependency here
go mod graph # dependency graph
The commands admit substantial visibility into the module graph.
Publishing
To publish a module:
- Push the code to a public repository (e.g., GitHub).
- Tag a release:
git tag v0.1.0 && git push --tags. - The module is now
go get-able by anyone.
The proxy fetches the module on demand; no explicit “publish” step is required.
A note on the conventional discipline
The contemporary Go modules advice:
- One
go.modper module; commit it andgo.sum. - Use semver-style tags (
v1.2.3) for releases. - Use
internal/for non-public code. - Use
cmd/for binaries; one subdirectory per binary. - Use
go mod tidyafter editing imports. - Use the proxy (default) for public modules.
- Use
replacefor local development; remove before pushing. - Pin major versions ≥ 2 in the import path (
/v2,/v3). - Use
go vet,go test,go buildin CI.
The combination — declarative go.mod, hash-locked go.sum, semver-based versioning, the proxy, the internal/ boundary, build tags for platform-specific code — is the substance of Go’s package management. The discipline produces reproducible, traceable, maintainable dependencies.