Standard Project Layout for Services and Libraries
Go has no official mandatory tree, but community conventions make repos predictable: cmd/ for binaries, internal/ for private code, and optional pkg/ for supported public APIs.
Matching that layout speeds onboarding and keeps module boundaries clear.
Summary
Place one main package per command under cmd/<name>/.
Keep implementation details in internal/ so other modules cannot import them.
Libraries meant for external reuse live in clearly named top-level packages or under pkg/ when you want an obvious public zone.
One module can host multiple commands and shared packages; split modules only when release lines diverge.
Recipe
Quick-reference recipe card - copy-paste ready.
example.com/shop/
go.mod
cmd/
shopd/main.go
migrate/main.go
internal/
api/
store/
pkg/client/ # optional stable SDK
// cmd/shopd/main.go
package main
import "example.com/shop/internal/api"
func main() {
api.ListenAndServe()
}When to reach for this:
- Starting a new HTTP service or worker with more than one binary.
- Publishing a library consumed by other companies or repos.
- Refactoring a flat repo where
mainand SQL mixed in one package.
Working Example
example.com/notify/
go.mod
README.md
cmd/
notifyd/main.go
internal/
config/config.go
server/http.go
queue/worker.go
pkg/client/client.go
// internal/config/config.go
package config
import "os"
func Port() string {
if p := os.Getenv("PORT"); p != "" {
return p
}
return "8080"
}// internal/server/http.go
package server
import (
"net/http"
"example.com/notify/internal/config"
)
func ListenAndServe() error {
return http.ListenAndServe(":"+config.Port(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
}// pkg/client/client.go - thin SDK for other services
package client
import "net/http"
type Client struct{ Base string }
func (c Client) Ping() (*http.Response, error) {
return http.Get(c.Base + "/healthz")
}// cmd/notifyd/main.go
package main
import (
"log"
"example.com/notify/internal/server"
)
func main() {
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}What this demonstrates:
cmd/notifydis the only executable entry; logic stays testable ininternal/server.pkg/clientis optional but signals supported API for other modules.- Configuration and HTTP wiring are private implementation details.
Deep Dive
How It Works
cmd/ - Each subdirectory is package main building one binary.
Names match the artifact (shopd, migrate, ctl).
CI maps go build -o bin/shopd ./cmd/shopd.
internal/ - Compiler blocks imports from outside the parent module subtree.
Use for storage layers, adapters, and anything not covered by semver promises.
pkg/ - Convention from Kubernetes-era repos; not enforced.
External importers may depend on it, so treat it like any public package with compatibility discipline.
Top-level domain packages - Many modules use example.com/widget/widget or example.com/widget with packages at root instead of pkg/.
Pick one story and document it in README.
Layout Comparison
| Layout | Best for | Watch out |
|---|---|---|
| Single cmd + internal | Small microservice | Growing god packages |
| cmd/* + internal/* | Multiple binaries | Duplicate flags/config |
| pkg/ client SDK | Platform libraries | Accidental breaking changes |
| Multi-module monorepo | Independent tags | go.work or replace overhead |
Go Notes
# Build all commands
go build -o bin/ ./cmd/...
# Test internal packages without exporting them
go test ./internal/...// Avoid business logic in main - keeps tests fast
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}Gotchas
- Treating pkg/ as magically stable - It is only convention; semver and docs make stability real.
- Everything in internal/ - Makes integration tests in other repos impossible; export minimal client surfaces.
- Multiple mains in one directory - Invalid; split binaries under
cmd/. - Flat repos that grow forever - Without
internal/, private helpers leak into public import paths. - Copying Kubernetes layout blindly - Your service may need one binary, not operators and CRDs.
Alternatives
- Separate modules per service - Strong isolation in large orgs.
- Domain-driven top-level packages (
billing/,shipping/) withoutpkg/- Clear for medium libraries. - Templates (kubebuilder, cobra) - Generate cmd/layout for specific problem domains.
FAQs
Is the Standard Go Project Layout official?
No - it is community guidance.
The Go blog documents modules and packages; layout is team choice within module rules.
Must libraries use pkg/?
No - many modules export from the module root or named folders.
Use pkg/ when you want a obvious external API zone.
How many commands belong in one module?
As many as share code and release cadence.
Split modules when binaries ship independently with different versions.
Where do integration tests go?
testdata/, internal/..._test.go, or top-level test/ directories.
Keep _test.go beside code for unit tests; use build tags for integration-only files.
Can internal packages import pkg/?
Yes - dependency flows inward: cmd -> internal -> (optional) shared public packages.
Avoid pkg importing internal (inverts the model).
Where should migrations live?
cmd/migrate, internal/migrate, or db/migrations/ SQL files.
Pick one tool (golang-migrate, goose) and document it.
Should configs be in internal/?
Yes for environment parsing and secrets wiring.
Expose only typed config structs needed by tests.
What about api/ vs internal/api/?
If HTTP handlers are not public import surfaces, keep them internal.
Public SDK belongs in pkg/ or a dedicated client module.
How do I layout a CLI with subcommands?
cmd/tool/main.go delegates to internal/cli using cobra or flag.
Each subcommand can be a file, not always a separate binary.
Does go mod init path affect layout?
Module path sets import prefixes.
Directory names should align with import paths for clarity.
Related
- Package Naming & internal/ Directories - Naming and internal rules
- Packages and Modules: Go's Unit of Distribution - Module vs package
- go work: Workspace Mode for Multi-Module Repos - Multi-module monorepos
- Packages & Modules Basics - Layout sketch example
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).