Go Style
Write Go code that is readable, maintainable, and predictable. Favour
clarity over cleverness; the language is small on purpose.
For the comprehensive idiomatic reference, see references/idioms.md. For
the catalogue of language gotchas, see references/pitfalls.md.
Decision shortcuts
Naming
- Packages: lowercase, singular, no underscores, no
util / common /
helpers. Name describes purpose.
- Variable length tracks scope:
i in a tight loop, customerOrderHistory
across a long function.
- Getters omit
Get; setters use Set. obj.Owner(), obj.SetOwner(u).
- Acronyms keep consistent case:
URL, ID, HTTP — never Url, Id,
Http.
- Constants are
mixedCaps, not SCREAMING_CASE.
- Interfaces with one method end in
-er: Reader, Closer, Stringer.
Use generics when
- Building a data structure that works across types (cache, tree, pool).
- Writing slice/map/channel utilities (filter, map, reduce).
- Type constraints remove runtime assertions.
Skip generics when the body would need type assertions anyway, when a
concrete type works, or when the result is harder to read. Prefer
slices, maps, and cmp (Go 1.21+) over hand-rolled utilities.
Interfaces
- Define interfaces at the consumption site, not next to the
implementation. The consumer declares only the methods it needs.
- Aim for 1 method, accept 2–3 if cohesive, split at 4+. Larger interfaces
are tolerable inside a SaaS product; keep them tiny in libraries.
- Accept interfaces, return concrete types.
any says nothing. Use a real interface or document the expected type.
Code organization
- Start as a few files in
package main. Add structure only when growth
demands it.
cmd/ for binaries, internal/ for code only this module may import,
pkg/ (or top-level packages) for the public API.
- One package, one purpose. Orient packages around a domain
(
package user), not an implementation accident (package models).
- Imports group as: stdlib → external → internal, blank-line separated.
Documentation
- Every exported symbol gets a comment that starts with the symbol's name
and forms a complete sentence ending with a period.
- Document why, not what the code already says.
- Package docs go above the
package clause in any one file (often
doc.go).
Modern Go quick reference
// Range over int (Go 1.22+)
for i := range 10 { ... }
// Generic data structure (Go 1.18+)
type Cache[K comparable, V any] struct { ... }
// Iterator (Go 1.23+)
func Positive(nums []int) iter.Seq[int] {
return func(yield func(int) bool) {
for _, n := range nums {
if n > 0 && !yield(n) {
return
}
}
}
}
// Prefer stdlib utilities
slices.Sort(items)
slices.SortFunc(items, func(a, b Item) int {
return cmp.Compare(a.Priority, b.Priority)
})
Configuration patterns
- Only
main() defines flags. Libraries take their config through a
constructor, never flag.String at package scope.
- Precedence: flags → environment → config file → default.
- Initialize structs with literals in one shot — never leave a struct
partially populated across multiple statements.
server := &http.Server{
Addr: addr,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
Handler: mux,
}
Common pitfalls (read references/pitfalls.md for the full set)
- Loop variable capture in closures — pass as parameter, or shadow
with
item := item before the closure.
- Nil interface vs nil value — an interface holding a typed nil is
not nil. Return explicit
nil when the underlying value is nil.
- Variable shadowing with
:= inside if blocks silently drops the
inner result. Use = to reassign.
- Defer in loops — defers fire at function exit, not loop iteration.
Wrap the body in a closure when you need per-iteration cleanup.
- Nil map writes panic. Always
make(map[K]V) before writing.
- Range copies values. Mutate via index:
for i := range items { items[i].x++ }.
- Slice reslicing shares the backing array. Use the three-index form
s[:n:n] to force a fresh allocation when needed.
When to load a sibling skill
| Task |
Skill |
Wrapping or matching errors, errors.Join |
go-errors |
| Goroutines, channels, context, errgroup |
go-concurrency |
log/slog, structured logging, observability |
go-logging |
Table-driven tests, t.Helper, integration gating |
go-testing |
| HTTP services, Chi router, graceful shutdown |
go-http |
CLI tools, subcommands, flag.NewFlagSet |
go-cli |
| sqlc, goose migrations, transactions |
go-sql |
golangci-lint, .golangci.yml, formatting |
go-lint |
Performance — measure first
Don't optimize without benchmarks. Once you have data:
- Preallocate when size is known:
make([]T, 0, n), make(map[K]V, n).
- Use
strings.Builder for iterative concatenation.
- On hot paths,
strconv.Itoa beats fmt.Sprint.
- Convert string→[]byte once outside the loop.
func BenchmarkProcess(b *testing.B) {
data := generateTestData()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Process(data)
}
}
Dependencies
- Libraries never vendor. Vendoring is for binaries.
internal/ is enforced by the compiler — only packages rooted at the
parent directory may import it. Use it to keep an API surface small.
1---2name: go-style3description: Idiomatic Go style: naming, package and file organization, interfaces, documentation, generics, iterators, range-over-int, performance, dependency management, and common pitfalls. ALWAYS use this skill when writing or reviewing Go code for general style and idioms — questions about naming conventions, where to put a type, when to use generics vs `any`, how to size interfaces, godoc comments, the `internal/` package, struct literal initialization, slice/map gotchas, loop-variable capture, defer timing, or "is this idiomatic Go?". Pair with go-errors, go-concurrency, go-testing, go-logging, go-http, go-cli, go-sql, or go-lint when those concerns dominate the task.4---56# Go Style78Write Go code that is readable, maintainable, and predictable. Favour9clarity over cleverness; the language is small on purpose.1011For the comprehensive idiomatic reference, see `references/idioms.md`. For12the catalogue of language gotchas, see `references/pitfalls.md`.1314## Decision shortcuts1516### Naming1718- Packages: lowercase, singular, no underscores, no `util` / `common` /19 `helpers`. Name describes purpose.20- Variable length tracks scope: `i` in a tight loop, `customerOrderHistory`21 across a long function.22- Getters omit `Get`; setters use `Set`. `obj.Owner()`, `obj.SetOwner(u)`.23- Acronyms keep consistent case: `URL`, `ID`, `HTTP` — never `Url`, `Id`,24 `Http`.25- Constants are `mixedCaps`, not `SCREAMING_CASE`.26- Interfaces with one method end in `-er`: `Reader`, `Closer`, `Stringer`.2728### Use generics when2930- Building a data structure that works across types (cache, tree, pool).31- Writing slice/map/channel utilities (filter, map, reduce).32- Type constraints remove runtime assertions.3334Skip generics when the body would need type assertions anyway, when a35concrete type works, or when the result is harder to read. Prefer36`slices`, `maps`, and `cmp` (Go 1.21+) over hand-rolled utilities.3738### Interfaces3940- Define interfaces at the **consumption site**, not next to the41 implementation. The consumer declares only the methods it needs.42- Aim for 1 method, accept 2–3 if cohesive, split at 4+. Larger interfaces43 are tolerable inside a SaaS product; keep them tiny in libraries.44- **Accept interfaces, return concrete types.**45- `any` says nothing. Use a real interface or document the expected type.4647### Code organization4849- Start as a few files in `package main`. Add structure only when growth50 demands it.51- `cmd/` for binaries, `internal/` for code only this module may import,52 `pkg/` (or top-level packages) for the public API.53- One package, one purpose. Orient packages around a domain54 (`package user`), not an implementation accident (`package models`).55- Imports group as: stdlib → external → internal, blank-line separated.5657### Documentation5859- Every exported symbol gets a comment that starts with the symbol's name60 and forms a complete sentence ending with a period.61- Document **why**, not what the code already says.62- Package docs go above the `package` clause in any one file (often63 `doc.go`).6465## Modern Go quick reference6667```go68// Range over int (Go 1.22+)69for i := range 10 { ... }7071// Generic data structure (Go 1.18+)72type Cache[K comparable, V any] struct { ... }7374// Iterator (Go 1.23+)75func Positive(nums []int) iter.Seq[int] {76 return func(yield func(int) bool) {77 for _, n := range nums {78 if n > 0 && !yield(n) {79 return80 }81 }82 }83}8485// Prefer stdlib utilities86slices.Sort(items)87slices.SortFunc(items, func(a, b Item) int {88 return cmp.Compare(a.Priority, b.Priority)89})90```9192## Configuration patterns9394- Only `main()` defines flags. Libraries take their config through a95 constructor, never `flag.String` at package scope.96- Precedence: flags → environment → config file → default.97- Initialize structs with literals in one shot — never leave a struct98 partially populated across multiple statements.99100```go101server := &http.Server{102 Addr: addr,103 ReadTimeout: 30 * time.Second,104 WriteTimeout: 30 * time.Second,105 Handler: mux,106}107```108109## Common pitfalls (read `references/pitfalls.md` for the full set)110111- **Loop variable capture in closures** — pass as parameter, or shadow112 with `item := item` before the closure.113- **Nil interface vs nil value** — an interface holding a typed nil is114 not nil. Return explicit `nil` when the underlying value is nil.115- **Variable shadowing with `:=`** inside `if` blocks silently drops the116 inner result. Use `=` to reassign.117- **Defer in loops** — defers fire at function exit, not loop iteration.118 Wrap the body in a closure when you need per-iteration cleanup.119- **Nil map writes panic.** Always `make(map[K]V)` before writing.120- **Range copies values.** Mutate via index: `for i := range items { items[i].x++ }`.121- **Slice reslicing shares the backing array.** Use the three-index form122 `s[:n:n]` to force a fresh allocation when needed.123124## When to load a sibling skill125126| Task | Skill |127|---|---|128| Wrapping or matching errors, `errors.Join` | go-errors |129| Goroutines, channels, context, errgroup | go-concurrency |130| `log/slog`, structured logging, observability | go-logging |131| Table-driven tests, `t.Helper`, integration gating | go-testing |132| HTTP services, Chi router, graceful shutdown | go-http |133| CLI tools, subcommands, `flag.NewFlagSet` | go-cli |134| sqlc, goose migrations, transactions | go-sql |135| `golangci-lint`, `.golangci.yml`, formatting | go-lint |136137## Performance — measure first138139Don't optimize without benchmarks. Once you have data:140141- Preallocate when size is known: `make([]T, 0, n)`, `make(map[K]V, n)`.142- Use `strings.Builder` for iterative concatenation.143- On hot paths, `strconv.Itoa` beats `fmt.Sprint`.144- Convert string→[]byte once outside the loop.145146```go147func BenchmarkProcess(b *testing.B) {148 data := generateTestData()149 b.ResetTimer()150 for i := 0; i < b.N; i++ {151 Process(data)152 }153}154```155156## Dependencies157158- Libraries never vendor. Vendoring is for binaries.159- `internal/` is enforced by the compiler — only packages rooted at the160 parent directory may import it. Use it to keep an API surface small.