Go Project Layout
Structure follows size. The biggest layout mistake in Go is copying a
microservice skeleton for a 500-line tool — or growing a 50-package
service inside a flat directory. Match the layout to the project.
1. Pick the Layout by Project Size
| Project |
Layout |
| Small tool, single binary, <5 files |
Flat: everything in package main at the root |
| Library for others to import |
Root package named after the module, internal/ for helpers |
| Service with one binary |
cmd/<name>/main.go + internal/ packages |
| Multiple binaries sharing code |
cmd/<name1>/, cmd/<name2>/ + internal/ |
Never start with empty pkg/, api/, docs/, build/ directories
"for later". Add structure when the code demands it, not before.
2. Module Naming
# ✅ Good — repository path, lowercase
go mod init github.com/acme/payment-service
# ❌ Bad — not fetchable, uppercase, or vanity without DNS
go mod init PaymentService
go mod init payment_service
The last path element should match what users will see: for a library,
it becomes the default import name.
3. Service Layout (the default for APIs and workers)
payment-service/
├── cmd/
│ └── payment-api/
│ └── main.go # flag/env parsing, wiring, Run() — nothing else
├── internal/
│ ├── domain/ # core types, business rules; zero external deps
│ ├── service/ # use cases orchestrating domain + stores
│ ├── store/ # data access implementations (postgres/, redis/)
│ ├── handler/ # HTTP/gRPC adapters
│ └── config/ # config loading and validation
├── migrations/ # if the service owns a database
├── go.mod
├── Makefile
└── README.md
Rules:
internal/ by default — the compiler enforces that nobody outside the
module imports it. Promote to a public package only on demand.
pkg/ only when external consumers exist AND the module also has
private code. When in doubt, don't create it.
- Dependencies point inward:
handler → service → domain ← store.
domain imports neither store nor handler.
4. Thin main, Runnable Run
Keep main.go to wiring plus a delegating call, so the app is testable:
func main() {
if err := run(context.Background(), os.Args[1:], os.Getenv); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(ctx context.Context, args []string, getenv func(string) string) error {
cfg, err := config.Load(getenv)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
db, err := store.Open(ctx, cfg.DatabaseURL)
if err != nil {
return fmt.Errorf("open db: %w", err)
}
defer db.Close()
svc := service.New(store.NewUserRepo(db))
srv := handler.NewServer(cfg.Addr, svc)
return srv.ListenAndServe(ctx)
}
os.Exit appears exactly once, in main.
run takes its dependencies (args, getenv) so tests can call it.
- No
init() functions for wiring — explicit construction order only.
5. Library Layout
retry/
├── retry.go # package retry — the API, in the root
├── retry_test.go
├── backoff.go # same package, split by topic
├── internal/
│ └── clock/ # implementation details users must not import
├── examples_test.go # Example* functions shown in godoc
└── go.mod
- The root directory IS the package. No
src/, no lib/.
- One package per concept. Resist
util, common, helpers — name
packages after what they provide (retry, clock, httpsign).
6. Naming Rules for Directories and Packages
- Package name == directory name, short, lowercase, no underscores:
store/postgres, not store/postgres_impl.
- Don't stutter:
payment.Service, not payment.PaymentService.
- Binary names in
cmd/ are user-facing: cmd/payment-api,
hyphenated is fine (directory only holds package main).
7. Files That Belong at the Root
go.mod, go.sum, README.md, LICENSE, Makefile,
.golangci.yml, Dockerfile (single-binary projects).
- Do NOT create:
src/ (un-idiomatic), vendor/ (unless the team
explicitly vendors), one-file packages like types/ or models/
that become dumping grounds.
Scaffolding Procedure
- Ask/decide: tool, library, or service? How many binaries?
go mod init <repo-path>.
- Create only the directories the first feature needs.
- Write
main.go with the thin-main pattern above.
- Add
Makefile targets: build, test, lint.
- Verify:
go build ./... and go vet ./... pass on the skeleton.
Verification Checklist
- Layout matches project size — no empty scaffolding directories
- Module path is the fetchable repository path
- All non-public packages live under
internal/
main.go is thin: parse, wire, call run, exit
os.Exit only in main; no wiring in init()
- Dependencies flow inward;
domain has zero infrastructure imports
- No
util/common/helpers/models grab-bag packages
- Package names match directories, lowercase, no stutter
go build ./... passes on the fresh skeleton
1---2name: go-project-layout3description: Scaffold new Go projects and services: directory structure, cmd/ and internal/ conventions, when to use a flat layout, module naming, and main package wiring. Use when: "new Go project", "scaffold a service", "create project structure", "start a Go module", "how do I organize a new service", "set up folder structure". Not for: reviewing an existing architecture (go-architecture-review), DI wiring (go-dependency-injection), CI setup (go-ci).4license: MIT5---6
7# Go Project Layout
8
9Structure follows size. The biggest layout mistake in Go is copying a
10microservice skeleton for a 500-line tool — or growing a 50-package
11service inside a flat directory. Match the layout to the project.
12
13## 1. Pick the Layout by Project Size
14
15| Project | Layout |
16|---|---|
17| Small tool, single binary, <5 files | Flat: everything in package `main` at the root |
18| Library for others to import | Root package named after the module, `internal/` for helpers |
19| Service with one binary | `cmd/<name>/main.go` + `internal/` packages |
20| Multiple binaries sharing code | `cmd/<name1>/`, `cmd/<name2>/` + `internal/` |
21
22Never start with empty `pkg/`, `api/`, `docs/`, `build/` directories
23"for later". Add structure when the code demands it, not before.
24
25## 2. Module Naming
26
27```bash
28# ✅ Good — repository path, lowercase
29go mod init github.com/acme/payment-service
30
31# ❌ Bad — not fetchable, uppercase, or vanity without DNS
32go mod init PaymentService
33go mod init payment_service
34```
35
36The last path element should match what users will see: for a library,
37it becomes the default import name.
38
39## 3. Service Layout (the default for APIs and workers)
40
41```text
42payment-service/
43├── cmd/
44│ └── payment-api/
45│ └── main.go # flag/env parsing, wiring, Run() — nothing else
46├── internal/
47│ ├── domain/ # core types, business rules; zero external deps
48│ ├── service/ # use cases orchestrating domain + stores
49│ ├── store/ # data access implementations (postgres/, redis/)
50│ ├── handler/ # HTTP/gRPC adapters
51│ └── config/ # config loading and validation
52├── migrations/ # if the service owns a database
53├── go.mod
54├── Makefile
55└── README.md
56```
57
58Rules:
59
60- `internal/` by default — the compiler enforces that nobody outside the
61 module imports it. Promote to a public package only on demand.
62- `pkg/` only when external consumers exist AND the module also has
63 private code. When in doubt, don't create it.
64- Dependencies point inward: `handler → service → domain ← store`.
65 `domain` imports neither `store` nor `handler`.
66
67## 4. Thin main, Runnable Run
68
69Keep `main.go` to wiring plus a delegating call, so the app is testable:
70
71```go
72func main() {
73 if err := run(context.Background(), os.Args[1:], os.Getenv); err != nil {
74 fmt.Fprintln(os.Stderr, err)
75 os.Exit(1)
76 }
77}
78
79func run(ctx context.Context, args []string, getenv func(string) string) error {
80 cfg, err := config.Load(getenv)
81 if err != nil {
82 return fmt.Errorf("load config: %w", err)
83 }
84
85 db, err := store.Open(ctx, cfg.DatabaseURL)
86 if err != nil {
87 return fmt.Errorf("open db: %w", err)
88 }
89 defer db.Close()
90
91 svc := service.New(store.NewUserRepo(db))
92 srv := handler.NewServer(cfg.Addr, svc)
93 return srv.ListenAndServe(ctx)
94}
95```
96
97- `os.Exit` appears exactly once, in `main`.
98- `run` takes its dependencies (`args`, `getenv`) so tests can call it.
99- No `init()` functions for wiring — explicit construction order only.
100
101## 5. Library Layout
102
103```text
104retry/
105├── retry.go # package retry — the API, in the root
106├── retry_test.go
107├── backoff.go # same package, split by topic
108├── internal/
109│ └── clock/ # implementation details users must not import
110├── examples_test.go # Example* functions shown in godoc
111└── go.mod
112```
113
114- The root directory IS the package. No `src/`, no `lib/`.
115- One package per concept. Resist `util`, `common`, `helpers` — name
116 packages after what they provide (`retry`, `clock`, `httpsign`).
117
118## 6. Naming Rules for Directories and Packages
119
120- Package name == directory name, short, lowercase, no underscores:
121 `store/postgres`, not `store/postgres_impl`.
122- Don't stutter: `payment.Service`, not `payment.PaymentService`.
123- Binary names in `cmd/` are user-facing: `cmd/payment-api`,
124 hyphenated is fine (directory only holds package `main`).
125
126## 7. Files That Belong at the Root
127
128- `go.mod`, `go.sum`, `README.md`, `LICENSE`, `Makefile`,
129 `.golangci.yml`, `Dockerfile` (single-binary projects).
130- Do NOT create: `src/` (un-idiomatic), `vendor/` (unless the team
131 explicitly vendors), one-file packages like `types/` or `models/`
132 that become dumping grounds.
133
134## Scaffolding Procedure
135
1361. Ask/decide: tool, library, or service? How many binaries?
1372. `go mod init <repo-path>`.
1383. Create only the directories the first feature needs.
1394. Write `main.go` with the thin-main pattern above.
1405. Add `Makefile` targets: `build`, `test`, `lint`.
1416. Verify: `go build ./...` and `go vet ./...` pass on the skeleton.
142
143## Verification Checklist
144
1451. Layout matches project size — no empty scaffolding directories
1462. Module path is the fetchable repository path
1473. All non-public packages live under `internal/`
1484. `main.go` is thin: parse, wire, call `run`, exit
1495. `os.Exit` only in `main`; no wiring in `init()`
1506. Dependencies flow inward; `domain` has zero infrastructure imports
1517. No `util`/`common`/`helpers`/`models` grab-bag packages
1528. Package names match directories, lowercase, no stutter
1539. `go build ./...` passes on the fresh skeleton