Go Packages and Modules: A Complete Guide
Go programs are organized into packages, and packages are distributed through modules. Understanding this hierarchy is essential for writing maintainable, reusable Go code. This guide covers everything from basic package organization to advanced module versioning.
Packages: The Building Blocks
Every Go file begins with a package declaration, and files in the same directory must belong to the same package. Packages group related functionality and control visibility.
Package Declaration
// math/operations.go
package math
func Add(a, b int) int {
return a + b
---
// private function — only visible within this package
func subtract(a, b int) int {
return a - b
---Exported names start with a capital letter. Unexported (private) names start with a lowercase letter and are only accessible within the same package. This is Go’s sole access-control mechanism — there are no public, private, or protected keywords.
Importing Packages
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Intn(100))
---- Standard library packages are referenced by their short name (e.g.,
"fmt","net/http") - Third-party packages include the module path (e.g.,
"github.com/gorilla/mux") - Import paths must be unique — the full import path identifies the package globally
Package Naming Conventions
- Package names are lowercase, single-word, no underscores or mixedCaps
- The package name matches the last element of the import path (
"encoding/json"→package json) - Avoid generic names like
utils,common, orhelpers - Use plural names for collections of utilities (
strings,bytes)
Internal Packages
Go has a special internal package convention. Packages inside an internal directory can only be imported by code rooted at the parent:
project/
├── internal/
│ └── secret/
│ └── keys.go // importable only within project/
├── cmd/
│ └── server/
│ └── main.go // can import project/internal/secret
└── external/
└── consumer/
└── main.go // CANNOT import project/internal/secretThe internal mechanism is enforced by the compiler, not by convention. It is Go’s primary tool for enforcing encapsulation boundaries without the overhead of separate repositories.
Modules: The Distribution Unit
A module is a collection of related Go packages versioned together. Modules are defined by a go.mod file at the repository root.
Creating a Module
mkdir myproject
cd myproject
go mod init github.com/username/myprojectThis produces:
module github.com/username/myproject
go 1.22The module path serves as the import path prefix for all packages within the module. If your module is github.com/username/myproject, the package at myproject/pkg/auth is imported as github.com/username/myproject/pkg/auth.
Adding Dependencies
# Add a specific version
go get github.com/gorilla/mux@v1.8.1
# Update to latest
go get -u github.com/gorilla/mux
# Add all missing and remove unused
go mod tidyThe go get command updates go.mod and creates go.sum, which contains cryptographic hashes of each dependency to verify integrity. The go.sum file should be committed to version control.
Key go.mod Directives
module github.com/example/myapp // module path
go 1.22 // Go version used
require ( // direct dependencies
github.com/gorilla/mux v1.8.1
github.com/lib/pq v1.10.9
)
require ( // indirect dependencies
golang.org/x/crypto v0.17.0 // needed by a direct dependency
)
exclude github.com/old/pkg v1.2.0 // blocks a specific version
replace github.com/old/pkg => ./local/pkg // replaces a module with local path
retract v1.0.0 // marks a version as unsafe/unpublishedThe replace directive is particularly useful for local development, letting you test against a local copy of a dependency without pushing changes.
Minimal Version Selection (MVS)
Go uses Minimal Version Selection (MVS) for dependency resolution. Unlike npm or Cargo’s resolver, MVS is simple and deterministic:
- Each module version is included at most once
- The minimum version satisfying all requirements is selected
- Newer versions are never downgraded because of an unrelated dependency
This algorithm avoids the “dependency hell” of other systems where the resolver must backtrack and try different version combinations. MVS guarantees that the build will use the oldest version that satisfies all constraints, which maximizes compatibility.
Working with Versions
Go modules use semantic versioning (semver) with some Go-specific conventions:
v1.0.0 // stable release
v1.0.1 // bug fix
v1.1.0 // new features (backward compatible)
v2.0.0 // breaking changes — module path becomes /v2
v0.0.0-20250101000000-abcdef // pseudo-version (from a commit)
v1.0.0-beta.1 // pre-releaseMajor Version Updates
When you make breaking changes, you must increment the major version and update the module path:
// go.mod for v2
module github.com/username/myproject/v2
// Importing v2
import "github.com/username/myproject/v2/pkg/auth"This is called “semantic import versioning.” Since Go modules can have multiple major versions installed simultaneously (they are different packages), the module path changes with each major version. This removes the need for package-level versioning conflicts.
Pseudo-versions
For code that has not been tagged, Go generates pseudo-versions:
v0.0.0-20250101000000-abcdef123456The format is vX.Y.Z-YYYYMMDDHHMMSS-commithash12. These are useful for testing pre-release code but should be replaced by proper tags for production.
Publishing a Module
- Tag the commit with a version:
git tag v1.0.0
git push origin v1.0.0- Go’s module proxy periodically fetches new tags. You can publish immediately:
GOPROXY=proxy.golang.org go list -m github.com/username/myproject@v1.0.0The Go module proxy (proxy.golang.org) is a read-through cache that stores module versions forever once fetched. This means a module cannot be “unpublished” after it is fetched — protecting the ecosystem from disappearing dependencies. If you need to retract a version, add a retract directive.
Module Checksum Database
Go’s checksum database (sum.golang.org) maintains an auditable log of module checksums. When you run go mod download, your module cache is verified against this database, ensuring that the code you download matches what the author published — even if the proxy is compromised.
Best Practices
Organizing Packages
- One package per directory — no exceptions
- Keep packages cohesive — a package should do one thing well
- Avoid cyclic imports — Go forbids them at compile time
- Prefer few, well-named packages over many tiny packages
- Use
internal/to hide implementation details
Dependency Hygiene
go mod tidy # clean up go.mod and go.sum
go mod verify # verify dependencies match checksums
go mod why # explain why a dependency is needed- Pin versions explicitly in production code
- Run
go mod tidybefore committing to keep the file clean - Use
go mod why -m <package>to understand why a dependency exists - Consider using
toolsdependencies for build-time tools:
// tools.go
//go:build tools
package main
import (
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
)The go.sum File
go.sum contains the expected cryptographic checksums for each dependency:
github.com/gorilla/mux v1.8.1 h1:TuMoUvkRETdXqU+FalsfPsRj9/7E7e4QreMJHaz8h0Y=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=Each line has the module path, version, hash algorithm (h1: = SHA-256), and the hash. The go.mod hash line ensures the content of go.mod matches. These checksums protect against both accidental corruption and malicious tampering.
Module Proxy and Checksum Database
Go uses a module proxy (proxy.golang.org) to cache and serve module versions. This proxy ensures that modules are never removed from the ecosystem once published. The checksum database (sum.golang.org) provides a verifiable log of module checksums, preventing supply-chain attacks.
Configuring Proxies
# Use direct access (no proxy)
GOPROXY=direct go get ./...
# Use multiple proxies with fallback
export GOPROXY=https://proxy.golang.org,direct
# Private modules (skip proxy)
export GOPROXY=https://proxy.golang.org,direct
export GONOSUMCHECK=github.com/mycompany/*
export GOPRIVATE=github.com/mycompany/*Workspace Mode (Go 1.18+)
For multi-module projects, workspace mode lets you work on multiple modules simultaneously:
go work init ./module1 ./module2
go work use ./local-modulego.work
├── use (
│ ./module1
│ ./module2
│ )
├── replace example.com/old => ./local-forkFAQ
Q: What is the difference between a module and a package?
A: A package is a directory of Go files with the same package declaration. A module is a collection of related packages versioned together, defined by a go.mod file.
Q: How do I update all dependencies at once?
A: Run go get -u ./... to update all direct and indirect dependencies to their latest minor/patch versions.
Q: What happens if two modules depend on different major versions of the same package?
A: Both versions can coexist because the major version is part of the import path (/v2, /v3). Go’s linker deduplicates the binary.
Q: Can I vendor my dependencies?
A: Yes, run go mod vendor to create a vendor/ directory. This is useful for CI reproducibility or air-gapped environments.
Q: How do I debug a dependency issue?
A: Use go mod graph to visualize the full dependency tree, go mod why to trace why a dependency is needed, and go mod verify to check for tampering.
Q: Should I commit go.sum?
A: Yes, always commit go.sum. It ensures reproducible builds and protects against supply-chain attacks.
Q: How do I use a local copy of a dependency?
A: Add a replace directive in go.mod: replace github.com/example/pkg => /local/path.
Q: What is a pseudo-version?
A: A version string generated from a git commit hash, used when no semver tag exists. Format: v0.0.0-20250101000000-abcdef123456.
For a comprehensive overview, read our article on Getting Started With Go.
For a comprehensive overview, read our article on Go Concurrency Guide.