Go Best Practices
Production patterns from Google, Uber, and the Go team. Updated for Go 1.25.
Sub-skills: skills/go-error-handling, skills/go-concurrency, skills/go-testing, skills/go-performance, skills/go-code-review, skills/go-linting, skills/go-project-layout, skills/go-security. Deep-dive references in references/.
Core Principles
Readable code prioritizes these attributes in order:
- Clarity: purpose and rationale are obvious to the reader
- Simplicity: accomplishes the goal in the simplest way
- Concision: high signal to noise ratio
- Maintainability: easy to modify correctly
- Consistency: matches surrounding codebase
Error Handling
Full guide: skills/go-error-handling/SKILL.md | Reference: references/error-handling.md
- Return errors, never panic in production code
- Wrap with
%w when callers need errors.Is/errors.As; use %v at boundaries
- Keep context succinct:
"new store: %w" not "failed to create new store: %w"
- Handle errors once: don't log and return the same error
- Error strings: lowercase, no punctuation
- Indent error flow: handle errors first, keep happy path at minimal indentation
- Use
errors.Join (Go 1.20+) for multiple independent failures
- Sentinel errors:
Err prefix for vars, Error suffix for types
if err != nil {
return fmt.Errorf("load config: %w", err)
}
Concurrency
Full guide: skills/go-concurrency/SKILL.md | Reference: references/concurrency.md
- Channel size: 0 (unbuffered) or 1; anything else needs justification
- Document goroutine lifetimes: when and how they exit
- Use
errgroup.Group over manual sync.WaitGroup for error-returning goroutines
- Prefer synchronous functions: let callers add concurrency
- Zero value mutexes: don't use pointers; don't embed in public structs
- Typed atomics (Go 1.19+):
atomic.Int64, atomic.Bool, atomic.Pointer[T]
sync.Map (Go 1.24+): significantly improved performance for disjoint key sets
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10)
for _, item := range items {
g.Go(func() error { return process(ctx, item) })
}
return g.Wait()
Naming
- MixedCaps always: never underscores (
MaxLength not MAX_LENGTH)
- Initialisms: consistent case (
URL, ID, HTTP not Url, Id, Http)
- Short variables: scope determines length (
i for loops, DefaultTimeout for globals)
- Receiver names: 1-2 letter abbreviation, consistent across methods, never
this/self
- Package names: lowercase single word, no
util/common/misc
- No repetition:
http.Serve not http.HTTPServe; c.WriteTo not c.WriteConfigTo
Pointer vs Value Receivers
| Pointer receiver |
Value receiver |
| Modifies receiver |
Small, immutable struct |
| Large struct |
Doesn't modify state |
| Contains sync.Mutex |
Map, func, or chan |
| Consistency with other methods |
Basic types |
Imports
import (
"context"
"fmt"
"github.com/google/uuid"
"golang.org/x/sync/errgroup"
"yourcompany/internal/config"
)
- Three groups: stdlib, external, internal (separated by blank lines)
- Rename only to avoid collisions
- No dot imports except test files with circular deps
- Blank imports (
import _ "pkg") only in main or tests
Module Management (Go 1.24+)
Track tool dependencies in go.mod with tool directives:
tool (
golang.org/x/tools/cmd/stringer
github.com/golangci/golangci-lint/cmd/golangci-lint
)
go get -tool golang.org/x/tools/cmd/stringer # add
go tool stringer -type=Status # run
go get tool # update all
go install tool # install to GOBIN
Structs
- Always use field names in initialization (positional breaks on changes)
- Omit zero value fields
- Don't embed types in public structs (exposes API unintentionally)
- Use
var for zero value structs: var user User
Slices and Maps
- Nil slices preferred:
var t []string (use []string{} only for JSON [] encoding)
- Copy at boundaries:
copy() or maps.Clone() to prevent mutation
- Preallocate:
make([]T, 0, len(input)) when size is known
- Use
slices and maps packages: slices.Sort, slices.Clone, maps.Clone, maps.Equal
Generics (Go 1.18+)
- Use when writing identical code for different types
- Use
cmp.Ordered or custom constraints for type safety
- Generic type aliases (Go 1.24+):
type Set[T comparable] = map[T]struct{}
- Don't over-generalize: use concrete types or interfaces when they suffice
func Filter[T any](s []T, pred func(T) bool) []T {
result := make([]T, 0, len(s))
for _, v := range s {
if pred(v) {
result = append(result, v)
}
}
return result
}
Iterators (Go 1.23+)
Range over functions for custom iterators:
func Backward[T any](s []T) func(yield func(int, T) bool) {
return func(yield func(int, T) bool) {
for i := len(s) - 1; i >= 0; i-- {
if !yield(i, s[i]) {
return
}
}
}
}
for i, v := range Backward(items) {
fmt.Println(i, v)
}
String/bytes iterators (Go 1.24+): strings.Lines, strings.SplitSeq, strings.SplitAfterSeq
Structured Logging (Go 1.21+)
slog.Info("user created", "id", userID, "email", email)
slog.With("service", "auth").Info("starting")
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})
slog.DiscardHandler (Go 1.24+) for suppressing logs in tests
- Use consistent key names, group related fields with
slog.Group
Performance
Full guide: skills/go-performance/SKILL.md | Reference: references/performance.md
strconv over fmt: strconv.Itoa(n) not fmt.Sprintf("%d", n)
- Avoid repeated
[]byte conversions: store once, reuse
- Preallocate map capacity:
make(map[string]int, len(items))
strings.Builder with Grow() for concatenation
Testing
Full guide: skills/go-testing/SKILL.md | Reference: references/testing.md
- Table-driven tests with
t.Parallel() for subtests
go-cmp over reflect.DeepEqual for clear diff output
- Useful failure messages: include input, got, want
t.Fatal for setup failures
- Interfaces belong to consumers, not producers
T.Context and T.Chdir (Go 1.24+)
b.Loop() (Go 1.24+): cleaner benchmarks, no b.ResetTimer() needed
synctest.Test (Go 1.25+): deterministic concurrent testing with synthetic time
Resource Management (Go 1.24+)
runtime.AddCleanup: multiple cleanups per object, no cycle leaks (replaces SetFinalizer)
weak.Pointer[T]: weak references for caches, canonicalization, observers
os.Root: scoped file access preventing path traversal attacks
Patterns
Full reference: references/patterns.md
- Functional options:
WithTimeout(d), WithLogger(l) for configurable constructors
- Interface compliance:
var _ http.Handler = (*Handler)(nil)
- Defer for cleanup: small overhead, worth the safety
- Graceful shutdown: signal handling +
srv.Shutdown(ctx)
- Enums start at one: zero = invalid/unset
- Use
time.Duration: never raw ints for time
- Two-value type assertions:
t, ok := i.(string) to avoid panics
- Context first:
func Process(ctx context.Context, ...)
- Avoid mutable globals: use dependency injection
- Avoid
init(): prefer explicit initialization in main
//go:embed (Go 1.16+): embed static files
- Field tags: explicit
json:"name" on marshaled structs
- Container-aware GOMAXPROCS (Go 1.25+): automatic cgroup-based tuning
Common Gotchas
Full reference: references/gotchas.md
| Gotcha |
Fix |
| Loop variable capture (pre-1.22) |
Fixed in Go 1.22+ (per-iteration vars) |
| Defer evaluates args immediately |
Capture in closure |
| Nil interface vs nil pointer |
Return nil explicitly |
| Use result before error check |
Always check err first (Go 1.25 enforces) |
| Map iteration order |
Sort keys with slices.Sorted(maps.Keys(m)) |
| Slice append shared backing |
Full slice expression a[:2:2] |
Linting
Full guide: skills/go-linting/SKILL.md
- Use golangci-lint as the standard linter aggregator
- Recommended linters: errcheck, govet, staticcheck, revive, gosimple, goimports, errorlint, bodyclose
- Add as a tool dependency (Go 1.24+):
go get -tool github.com/golangci/golangci-lint/cmd/golangci-lint
- Run in CI: use
golangci/golangci-lint-action for GitHub Actions
- Suppress sparingly: prefer fixing over
//nolint comments
Project Layout
Full guide: skills/go-project-layout/SKILL.md
cmd/: one subdirectory per executable, keep main.go thin
internal/: private packages, enforced by the Go toolchain
- Avoid
pkg/, src/, models/, utils/: name packages by purpose
- Flat is fine: small projects should not have deep directory trees
- Dockerfile: multi-stage build,
CGO_ENABLED=0, distroless base image
Security
Full guide: skills/go-security/SKILL.md
- Parameterized SQL queries: never interpolate user input
os.Root (Go 1.24+): scoped file access preventing path traversal
- Validate at boundaries: decode into typed structs, validate fields
- Never hardcode or log secrets: use
Secret type with redacted String()
- Standard crypto only:
crypto/rand for random bytes, bcrypt for passwords
- HTTP timeouts: always set
ReadTimeout, WriteTimeout, IdleTimeout
govulncheck: scan dependencies for known vulnerabilities
go test -race: always run with the race detector in CI
Experimental (Go 1.25)
encoding/json/v2: enable with GOEXPERIMENT=jsonv2. Better performance, streaming, custom marshalers per call.
Documentation
- Comments are full sentences starting with the declared name
- Package comments: before
package declaration, no blank line
References
- Google Go Style Guide
- Uber Go Style Guide
- Effective Go
- Go Code Review Comments
- Go 1.23 Release Notes
- Go 1.24 Release Notes
- Go 1.25 Release Notes
1---2name: golang3description: Use when writing, reviewing, or refactoring Go code. Provides production best practices for Go covering error handling, concurrency, naming, testing, performance, generics, iterators, and common pitfalls. Distilled from Google Go Style Guide, Uber Go Style Guide, Effective Go, and Go Code Review Comments. Updated for Go 1.25.4license: MIT5---67# Go Best Practices89Production patterns from Google, Uber, and the Go team. Updated for Go 1.25.1011> Sub-skills: `skills/go-error-handling`, `skills/go-concurrency`, `skills/go-testing`, `skills/go-performance`, `skills/go-code-review`, `skills/go-linting`, `skills/go-project-layout`, `skills/go-security`. Deep-dive references in `references/`.1213## Core Principles1415Readable code prioritizes these attributes in order:16171. **Clarity**: purpose and rationale are obvious to the reader182. **Simplicity**: accomplishes the goal in the simplest way193. **Concision**: high signal to noise ratio204. **Maintainability**: easy to modify correctly215. **Consistency**: matches surrounding codebase2223---2425## Error Handling2627> Full guide: `skills/go-error-handling/SKILL.md` | Reference: `references/error-handling.md`2829- **Return errors, never panic** in production code30- **Wrap with `%w`** when callers need `errors.Is`/`errors.As`; use `%v` at boundaries31- **Keep context succinct**: `"new store: %w"` not `"failed to create new store: %w"`32- **Handle errors once**: don't log and return the same error33- **Error strings**: lowercase, no punctuation34- **Indent error flow**: handle errors first, keep happy path at minimal indentation35- **Use `errors.Join`** (Go 1.20+) for multiple independent failures36- **Sentinel errors**: `Err` prefix for vars, `Error` suffix for types3738```go39if err != nil {40 return fmt.Errorf("load config: %w", err)41}42```4344---4546## Concurrency4748> Full guide: `skills/go-concurrency/SKILL.md` | Reference: `references/concurrency.md`4950- **Channel size**: 0 (unbuffered) or 1; anything else needs justification51- **Document goroutine lifetimes**: when and how they exit52- **Use `errgroup.Group`** over manual `sync.WaitGroup` for error-returning goroutines53- **Prefer synchronous functions**: let callers add concurrency54- **Zero value mutexes**: don't use pointers; don't embed in public structs55- **Typed atomics** (Go 1.19+): `atomic.Int64`, `atomic.Bool`, `atomic.Pointer[T]`56- **`sync.Map`** (Go 1.24+): significantly improved performance for disjoint key sets5758```go59g, ctx := errgroup.WithContext(ctx)60g.SetLimit(10)61for _, item := range items {62 g.Go(func() error { return process(ctx, item) })63}64return g.Wait()65```6667---6869## Naming7071- **MixedCaps always**: never underscores (`MaxLength` not `MAX_LENGTH`)72- **Initialisms**: consistent case (`URL`, `ID`, `HTTP` not `Url`, `Id`, `Http`)73- **Short variables**: scope determines length (`i` for loops, `DefaultTimeout` for globals)74- **Receiver names**: 1-2 letter abbreviation, consistent across methods, never `this`/`self`75- **Package names**: lowercase single word, no `util`/`common`/`misc`76- **No repetition**: `http.Serve` not `http.HTTPServe`; `c.WriteTo` not `c.WriteConfigTo`7778### Pointer vs Value Receivers7980| Pointer receiver | Value receiver |81|---|---|82| Modifies receiver | Small, immutable struct |83| Large struct | Doesn't modify state |84| Contains sync.Mutex | Map, func, or chan |85| Consistency with other methods | Basic types |8687---8889## Imports9091```go92import (93 "context"94 "fmt"9596 "github.com/google/uuid"97 "golang.org/x/sync/errgroup"9899 "yourcompany/internal/config"100)101```102103- Three groups: stdlib, external, internal (separated by blank lines)104- Rename only to avoid collisions105- No dot imports except test files with circular deps106- Blank imports (`import _ "pkg"`) only in main or tests107108---109110## Module Management (Go 1.24+)111112Track tool dependencies in `go.mod` with tool directives:113114```go115tool (116 golang.org/x/tools/cmd/stringer117 github.com/golangci/golangci-lint/cmd/golangci-lint118)119```120121```bash122go get -tool golang.org/x/tools/cmd/stringer # add123go tool stringer -type=Status # run124go get tool # update all125go install tool # install to GOBIN126```127128---129130## Structs131132- **Always use field names** in initialization (positional breaks on changes)133- **Omit zero value fields**134- **Don't embed types** in public structs (exposes API unintentionally)135- **Use `var` for zero value structs**: `var user User`136137---138139## Slices and Maps140141- **Nil slices preferred**: `var t []string` (use `[]string{}` only for JSON `[]` encoding)142- **Copy at boundaries**: `copy()` or `maps.Clone()` to prevent mutation143- **Preallocate**: `make([]T, 0, len(input))` when size is known144- **Use `slices` and `maps` packages**: `slices.Sort`, `slices.Clone`, `maps.Clone`, `maps.Equal`145146---147148## Generics (Go 1.18+)149150- Use when writing identical code for different types151- Use `cmp.Ordered` or custom constraints for type safety152- **Generic type aliases** (Go 1.24+): `type Set[T comparable] = map[T]struct{}`153- **Don't over-generalize**: use concrete types or interfaces when they suffice154155```go156func Filter[T any](s []T, pred func(T) bool) []T {157 result := make([]T, 0, len(s))158 for _, v := range s {159 if pred(v) {160 result = append(result, v)161 }162 }163 return result164}165```166167---168169## Iterators (Go 1.23+)170171Range over functions for custom iterators:172173```go174func Backward[T any](s []T) func(yield func(int, T) bool) {175 return func(yield func(int, T) bool) {176 for i := len(s) - 1; i >= 0; i-- {177 if !yield(i, s[i]) {178 return179 }180 }181 }182}183184for i, v := range Backward(items) {185 fmt.Println(i, v)186}187```188189String/bytes iterators (Go 1.24+): `strings.Lines`, `strings.SplitSeq`, `strings.SplitAfterSeq`190191---192193## Structured Logging (Go 1.21+)194195```go196slog.Info("user created", "id", userID, "email", email)197slog.With("service", "auth").Info("starting")198handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})199```200201- `slog.DiscardHandler` (Go 1.24+) for suppressing logs in tests202- Use consistent key names, group related fields with `slog.Group`203204---205206## Performance207208> Full guide: `skills/go-performance/SKILL.md` | Reference: `references/performance.md`209210- **`strconv` over `fmt`**: `strconv.Itoa(n)` not `fmt.Sprintf("%d", n)`211- **Avoid repeated `[]byte` conversions**: store once, reuse212- **Preallocate map capacity**: `make(map[string]int, len(items))`213- **`strings.Builder`** with `Grow()` for concatenation214215---216217## Testing218219> Full guide: `skills/go-testing/SKILL.md` | Reference: `references/testing.md`220221- **Table-driven tests** with `t.Parallel()` for subtests222- **`go-cmp`** over `reflect.DeepEqual` for clear diff output223- **Useful failure messages**: include input, got, want224- **`t.Fatal`** for setup failures225- **Interfaces belong to consumers**, not producers226- **`T.Context`** and **`T.Chdir`** (Go 1.24+)227- **`b.Loop()`** (Go 1.24+): cleaner benchmarks, no `b.ResetTimer()` needed228- **`synctest.Test`** (Go 1.25+): deterministic concurrent testing with synthetic time229230---231232## Resource Management (Go 1.24+)233234- **`runtime.AddCleanup`**: multiple cleanups per object, no cycle leaks (replaces `SetFinalizer`)235- **`weak.Pointer[T]`**: weak references for caches, canonicalization, observers236- **`os.Root`**: scoped file access preventing path traversal attacks237238---239240## Patterns241242> Full reference: `references/patterns.md`243244- **Functional options**: `WithTimeout(d)`, `WithLogger(l)` for configurable constructors245- **Interface compliance**: `var _ http.Handler = (*Handler)(nil)`246- **Defer for cleanup**: small overhead, worth the safety247- **Graceful shutdown**: signal handling + `srv.Shutdown(ctx)`248- **Enums start at one**: zero = invalid/unset249- **Use `time.Duration`**: never raw ints for time250- **Two-value type assertions**: `t, ok := i.(string)` to avoid panics251- **Context first**: `func Process(ctx context.Context, ...)`252- **Avoid mutable globals**: use dependency injection253- **Avoid `init()`**: prefer explicit initialization in `main`254- **`//go:embed`** (Go 1.16+): embed static files255- **Field tags**: explicit `json:"name"` on marshaled structs256- **Container-aware GOMAXPROCS** (Go 1.25+): automatic cgroup-based tuning257258---259260## Common Gotchas261262> Full reference: `references/gotchas.md`263264| Gotcha | Fix |265|--------|-----|266| Loop variable capture (pre-1.22) | Fixed in Go 1.22+ (per-iteration vars) |267| Defer evaluates args immediately | Capture in closure |268| Nil interface vs nil pointer | Return `nil` explicitly |269| Use result before error check | Always check `err` first (Go 1.25 enforces) |270| Map iteration order | Sort keys with `slices.Sorted(maps.Keys(m))` |271| Slice append shared backing | Full slice expression `a[:2:2]` |272273---274275## Linting276277> Full guide: `skills/go-linting/SKILL.md`278279- **Use golangci-lint** as the standard linter aggregator280- **Recommended linters**: errcheck, govet, staticcheck, revive, gosimple, goimports, errorlint, bodyclose281- **Add as a tool dependency** (Go 1.24+): `go get -tool github.com/golangci/golangci-lint/cmd/golangci-lint`282- **Run in CI**: use `golangci/golangci-lint-action` for GitHub Actions283- **Suppress sparingly**: prefer fixing over `//nolint` comments284285---286287## Project Layout288289> Full guide: `skills/go-project-layout/SKILL.md`290291- **`cmd/`**: one subdirectory per executable, keep `main.go` thin292- **`internal/`**: private packages, enforced by the Go toolchain293- **Avoid `pkg/`, `src/`, `models/`, `utils/`**: name packages by purpose294- **Flat is fine**: small projects should not have deep directory trees295- **Dockerfile**: multi-stage build, `CGO_ENABLED=0`, distroless base image296297---298299## Security300301> Full guide: `skills/go-security/SKILL.md`302303- **Parameterized SQL queries**: never interpolate user input304- **`os.Root`** (Go 1.24+): scoped file access preventing path traversal305- **Validate at boundaries**: decode into typed structs, validate fields306- **Never hardcode or log secrets**: use `Secret` type with redacted `String()`307- **Standard crypto only**: `crypto/rand` for random bytes, `bcrypt` for passwords308- **HTTP timeouts**: always set `ReadTimeout`, `WriteTimeout`, `IdleTimeout`309- **`govulncheck`**: scan dependencies for known vulnerabilities310- **`go test -race`**: always run with the race detector in CI311312---313314## Experimental (Go 1.25)315316- **`encoding/json/v2`**: enable with `GOEXPERIMENT=jsonv2`. Better performance, streaming, custom marshalers per call.317318---319320## Documentation321322- Comments are full sentences starting with the declared name323- Package comments: before `package` declaration, no blank line324325---326327## References3283291. [Google Go Style Guide](https://google.github.io/styleguide/go/)3302. [Uber Go Style Guide](https://github.com/uber-go/guide)3313. [Effective Go](https://go.dev/doc/effective_go)3324. [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments)3335. [Go 1.23 Release Notes](https://go.dev/doc/go1.23)3346. [Go 1.24 Release Notes](https://go.dev/doc/go1.24)3357. [Go 1.25 Release Notes](https://go.dev/doc/go1.25)