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.
Search across all documentation pages
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.
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.
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:
main and SQL mixed in one package.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/notifyd is the only executable entry; logic stays testable in internal/server.pkg/client is optional but signals supported API for other modules.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 | 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 |
# 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)
}
}cmd/.internal/, private helpers leak into public import paths.billing/, shipping/) without pkg/ - Clear for medium libraries.No - it is community guidance.
The Go blog documents modules and packages; layout is team choice within module rules.
No - many modules export from the module root or named folders.
Use pkg/ when you want a obvious external API zone.
As many as share code and release cadence.
Split modules when binaries ship independently with different versions.
testdata/, internal/..._test.go, or top-level test/ directories.
Keep _test.go beside code for unit tests; use build tags for integration-only files.
Yes - dependency flows inward: cmd -> internal -> (optional) shared public packages.
Avoid pkg importing internal (inverts the model).
cmd/migrate, internal/migrate, or db/migrations/ SQL files.
Pick one tool (golang-migrate, goose) and document it.
Yes for environment parsing and secrets wiring.
Expose only typed config structs needed by tests.
If HTTP handlers are not public import surfaces, keep them internal.
Public SDK belongs in pkg/ or a dedicated client module.
cmd/tool/main.go delegates to internal/cli using cobra or flag.
Each subcommand can be a file, not always a separate binary.
Module path sets import prefixes.
Directory names should align with import paths for clarity.
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).
Reviewed by Chris St. John·Last updated Jul 19, 2026