21 — Packages & Modules
Package Organization
go
// ┌──────────────────────────────────────────────────────────────────────┐
// │ Package = directory of .go files with the same `package` clause │
// │ Module = versioned collection of packages, defined by go.mod │
// │ Import = module path + subdirectory path │
// │ │
// │ Visibility: │
// │ PascalCase = exported (public) │
// │ camelCase = unexported (package-private) │
// │ internal/ = toolchain-enforced private (only within parent) │
// └──────────────────────────────────────────────────────────────────────┘
// File structure:
// example.com/myapp/
// ├── go.mod # module example.com/myapp
// ├── go.sum # dependency hashes
// ├── cmd/myapp/main.go # package main — entry point
// ├── internal/
// │ ├── config/config.go # package config — private to myapp
// │ ├── domain/user.go # package domain — business types
// │ └── store/postgres/ # package postgres — DB impl
// └── pkg/
// └── ratelimiter/ # package ratelimiter — public, reusable
The internal/ Enforcement
go
// Packages under internal/ can ONLY be imported by code within the
// parent of internal/. The Go TOOLCHAIN enforces this at compile time.
// example.com/myapp/internal/config/config.go
package config
type Config struct {
DSN string
}
// This can be imported by:
// ✅ example.com/myapp/cmd/myapp/main.go
// ✅ example.com/myapp/internal/store/store.go
// ❌ example.com/other-app/main.go — compile error: "use of internal package not allowed"
// ❌ github.com/some/lib/lib.go — compile error
Module Versioning Semantics
bash
# Go uses Semantic Versioning: vMAJOR.MINOR.PATCH
# v1.2.3 → major=1, minor=2, patch=3
#
# Import paths for v2+ include the major version:
# v1: github.com/lib/pq → import "github.com/lib/pq"
# v2: github.com/foo/bar/v2 → import "github.com/foo/bar/v2"
# v3: github.com/foo/bar/v3 → import "github.com/foo/bar/v3"
#
# v0.x and v1.x don't have a version suffix in the import path.
# v2+ MUST have /v2, /v3, etc. in the module path and import path.
# Add/update dependencies:
go get github.com/lib/pq@latest # latest stable
go get github.com/lib/pq@v1.10.9 # exact version
go get github.com/lib/pq@v1.10.0-beta.1 # pre-release
go get github.com/some/repo@main # ⚠️ pseudo-version (unstable)
# Tidy — add missing, remove unused, update go.sum:
go mod tidy
# Upgrade a dependency:
go get github.com/lib/pq@latest
go mod tidy
# Downgrade:
go get github.com/lib/pq@v1.9.0
replace Directives — Local Development
go
// go.mod:
// replace example.com/mylib => ../mylib
// replace github.com/lib/pq => github.com/myfork/pq v1.10.1
// ❌ ANTI-PATTERN: using replace in a PUBLISHED library's go.mod
// Consumers inherit the replace — breaks if they don't have the local path.
// Never commit replace directives in library go.mod files.
// ✅ CORRECT: use go.work for local dev (gitignored)
// go.work (developer-local):
// use ./myapp
// use ../mylib
// go.mod stays clean (no replace) for CI and publishing.
// ✅ Legitimate replace: security fork in an APPLICATION (not library):
// replace github.com/vulnerable/dep => github.com/patched/dep v1.2.1
Private Modules
bash
# Private modules need GOPRIVATE so the Go tool doesn't try the public proxy:
go env -w GOPRIVATE=github.com/yourorg/*
# Or for a specific host:
go env -w GOPRIVATE=gitlab.com/yourcompany/*
# Without GOPRIVATE, `go get` from a private repo fails with 404
# (the Go proxy can't access private repos).
# Auth: configure git credentials:
# ~/.netrc:
# machine github.com
# login your-token
# password ghp_xxxxx
# Or use SSH keys (go uses git for fetching).
# GOINSECURE for self-hosted HTTP (no TLS):
go env -w GOINSECURE=internal.company.git/*
go.sum — Supply Chain Verification
bash
# go.sum records SHA-256 hashes of every dependency's zip + go.mod.
# The Go tool verifies these on download — if a registry serves a
# different hash, the build FAILS. This is supply-chain protection.
# Verify all cached modules match go.sum:
go mod verify
# Why go.sum matters:
# 1. Reproducibility — same go.sum → same dependencies
# 2. Security — tampered dependencies fail to build
# 3. Auditing — you can see exactly what versions were used
# ⚠️ go.sum should be COMMITTED to version control.
# go.mod and go.sum together define the build — both must be in git.
Workspaces (go work)
bash
# go work lets you develop multiple modules simultaneously, with local
# edits taking effect without replace directives.
mkdir workspace && cd workspace
go work init ./myapp ./lib/mylib
# go.work:
# go 1.22
# use ./myapp
# use ./lib/mylib
# Now `go build` in ./myapp resolves imports of example.com/mylib
# to the LOCAL ./lib/mylib — no replace directive needed.
# go.work is developer-local — gitignore it (don't commit).
# Sync go.work module versions back to go.mod:
go work sync
Version Upgrades — Breaking Changes
bash
# Check for outdated dependencies:
go list -m -u all # shows current and latest versions
# Upgrade a single dependency:
go get github.com/lib/pq@latest
go mod tidy
go test ./... # ⚠️ always test after upgrading
# Upgrade ALL dependencies (careful — may break things):
go get -u ./...
go mod tidy
go test ./...
# Major version upgrade (v1 → v2):
# 1. Change the import path: "github.com/foo/bar" → "github.com/foo/bar/v2"
# 2. go mod tidy
# 3. Fix breaking API changes (v2 may have different signatures)
# 4. go test ./...
# ⚠️ `go get -u` in CI is dangerous — it can break the build if a
# dependency releases a breaking change. Pin versions in go.mod and
# upgrade deliberately.
💡 Tips & Tricks
- Idiom: use
internal/for packages that shouldn't be imported outside your module — the toolchain enforces the boundary. Reservepkg/for genuinely public, reusable packages. - Idiom:
go mod tidybefore every commit — it adds missing deps and removes unused ones, keeping go.mod/go.sum accurate. A CI checkgo mod tidy && git diff --exit-codeprevents drift. - Idiom: use
go work(Go 1.18+) for local multi-module development — it lets local edits to a library take effect in the app withoutreplacedirectives in go.mod (which break CI). - Idiom: pin dependencies to specific versions in go.mod (committed) —
go get @latestin CI can break the build. Upgrade deliberately, test after. - Safety: commit both go.mod AND go.sum — they together define the build. go.sum provides supply-chain verification (hashes). Don't gitignore go.sum.
- Idiom: use GOPRIVATE for private repos so the Go tool uses git auth instead of the public proxy. Without it,
go getfails with 404.
⚠️ Edge Cases & Gotchas
- v2+ import paths require
/v2:import "github.com/foo/bar/v2"— without the suffix, you get v1. The module's go.mod must also havemodule github.com/foo/bar/v2. replacein a library breaks consumers: never commitreplacein a library's go.mod — consumers inherit it and break if they lack the local path. Usego.workinstead.- Pseudo-versions are unstable:
go get foo@maincreates a pseudo-version tied to a commit hash. The commit changes → different build. Pin to tags for reproducibility. go mod tidymay remove deps: if you remove an import but don't run tidy, go.mod still lists the dep. Tidy cleans up — but also adds missing ones. Run it before committing.go.sumcan have extra entries: after an upgrade, go.sum may have hashes for multiple versions.go mod tidycleans up unused entries.- Private modules need GOPRIVATE: without it,
go gettries the public proxy (returns 404 for private repos). Setgo env -w GOPRIVATE=github.com/yourorg/*. GOFLAGS=-mod=readonlyprevents accidental go.mod changes: in CI, use this to fail the build if go.mod needs modification. In dev,-mod=modallows auto-updates.internal/is enforced by the toolchain, not the filesystem: the boundary is based on the module path, not the directory. A symlink outside the module can still import internal/.
🧠 Quick Quiz
bash
# go.mod:
# module example.com/myapp
# require github.com/lib/pq v1.10.0
#
# go get github.com/lib/pq@latest # v1.10.9 is latest
# What does go.mod say now?
Answer
go.mod now says:
require github.com/lib/pq v1.10.9
go get @latest updates the require directive to the latest version. go.sum is also updated with the v1.10.9 hash.
But this is a minor version upgrade (v1.10.0 → v1.10.9) — within the same major version (v1), so no import path change is needed.
If the latest were v2.0.0, the import path would need to change to github.com/lib/pq/v2 — a breaking upgrade requiring code changes.
📚 What's Next
→ 22 — I/O, Files & the io Package — Reader/Writer composition, io.Copy, buffered I/O, and streaming patterns.