Packages, project layout and dependencies
Package naming and internal packages, cmd and pkg layout, adding and upgrading modules, vendoring, and semantic import versioning.
Packages and internal
A package is a directory. Every file in it declares the same package name, and the whole package is compiled as a unit, so files can share unexported identifiers freely.
notes/
go.mod
cmd/notes/main.go package main -> import path example.com/notes/cmd/notes
internal/store/store.go package store -> importable only inside example.com/notes
internal/config/config.go
user.go package notes -> the public surface of the library
user_test.go- Name packages after what they provide, not after a layer:
store,auth,pricing— notutil,commonorhelpers, which attract unrelated code forever. - Do not repeat the package name in identifiers.
store.Storeis fine;store.StoreStoreis not.http.Serverandbytes.Bufferread well because the qualifier and the name differ. - Everything under
internal/can be imported only by packages rooted at that directory's parent. It is enforced by the compiler and is the correct way to keep an API private while still splitting code across directories. - Avoid a deep hierarchy of tiny packages. Dependencies flow one way, and a package that imports thirty others is usually two packages.
Choosing a layout
| Directory | Put here |
|---|---|
cmd/<name>/ | One thin package main per binary: flag parsing, wiring, then a call into the library |
| Root package | The domain types and the operations on them, so import "example.com/notes" is enough for a library |
internal/ | Implementation packages you are not ready to support as public API |
pkg/ | Rarely needed. Historically a marker for "public sub-packages"; on internal/ projects it usually adds a meaningless level |
testdata/ | Fixture files; the toolchain deliberately ignores this directory name |
scripts/, Makefile | Build and release glue that is not Go code |
// cmd/notes/main.go — the only place that knows the whole application
package main
import (
"example.com/notes/internal/config"
"example.com/notes/internal/store"
)
func main() {
cfg, err := config.Load("config.toml")
if err != nil {
log.Fatal(err) // logging at the boundary, once
}
s := store.New(cfg.DatabaseURL)
run(context.Background(), cfg, s)
}When the same library grows a second binary, promote the shared wiring into a package and keep the cmd files to flags plus one call. That is the whole test for a layout: can a new binary be added without moving code?
Dependencies and versioning
go get github.com/spf13/[email protected] # pin an exact version
go get -u=patch ./... # apply patch upgrades only
go list -m -u all # what could be upgraded
go mod why github.com/some/dep # why it is required at all
go mod verify # check the cache against go.sum
go mod vendor
go build -mod=vendor ./... # build from vendor/, no network- Semantic import versioning: a module at v2 or above carries the major version in its path —
github.com/x/y/v2. Two majors can therefore be required at once without conflict. - A
// indirectcomment means nothing in your code imports that module directly; it is present because a dependency needs it. Do not hand-edit those lines. - Vendoring commits the dependency source into the repository. It makes builds work without a proxy and makes review show exactly what changed, at the cost of a large diff.
- Upgrade deliberately and in one commit per dependency group, then run the full test suite. Bulk
-u ./...upgrades are how a working service picks up three breaking changes at once. - The minimum version selection rule means Go never surprises you with a newer version than
go.modnames — you always get the versions you asked for.
💡
Before adding a dependency, check whether the standard library already does it:
slices, maps, slog, min, max, errors.Join and net/http replaced a large share of popular third-party packages. Fewer dependencies is not purity, it is less code you have to upgrade.FAQ
Does internal work across modules?
Yes. A path containing an
internal element is importable only from the package tree rooted at the parent of that directory, whether the import comes from the same module or another one.Should every project use the cmd plus internal layout?
No. A single-purpose program is fine as one root package with a
main.go. Add cmd/ when a second binary appears, and internal/ when you want to keep code out of your public API.Related
Setting up Go: modules, toolchain and editors Profiling, performance and deployment
Last refreshed 2026-09-18.