golang
Production Go code patterns covering error handling, concurrency, naming, testing, and performance.
- Comprehensive error handling guidance: return errors instead of panicking, use error wrapping with
%wfor inspection and%vfor boundaries, join multiple errors witherrors.Join, and handle errors once at the appropriate level - Concurrency best practices: unbuffered or size-1 channels, document goroutine lifetimes, prefer
errgroup.Groupfor concurrent operations, and use typed atomics fromsync/atomic - Naming conventions: MixedCaps always, consistent initialisms (URL not Url), short names for limited scope, one or two-letter receiver names, and avoid repeating package names in identifiers
- Testing patterns: table-driven tests with parallel execution, use
go-cmpfor comparisons,synctestfor deterministic concurrent testing (Go 1.25+), and verify interface compliance at compile time - Performance tips: preallocate slices and maps with known capacity, use
strings.Builderfor concatenation, preferstrconvoverfmt, and leverage standard libraryslicesandmapspackages
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 inreferences/.
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
%wwhen callers neederrors.Is/errors.As; use%vat 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:
Errprefix for vars,Errorsuffix 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.Groupover manualsync.WaitGroupfor 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 (
MaxLengthnotMAX_LENGTH) - Initialisms: consistent case (
URL,ID,HTTPnotUrl,Id,Http) - Short variables: scope determines length (
ifor loops,DefaultTimeoutfor 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.Servenothttp.HTTPServe;c.WriteTonotc.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
varfor 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()ormaps.Clone()to prevent mutation - Preallocate:
make([]T, 0, len(input))when size is known - Use
slicesandmapspackages:slices.Sort,slices.Clone,maps.Clone,maps.Equal
Generics (Go 1.18+)
- Use when writing identical code for different types
- Use
cmp.Orderedor 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
strconvoverfmt:strconv.Itoa(n)notfmt.Sprintf("%d", n)- Avoid repeated
[]byteconversions: store once, reuse - Preallocate map capacity:
make(map[string]int, len(items)) strings.BuilderwithGrow()for concatenation
Testing
Full guide:
skills/go-testing/SKILL.md| Reference:references/testing.md
- Table-driven tests with
t.Parallel()for subtests go-cmpoverreflect.DeepEqualfor clear diff output- Useful failure messages: include input, got, want
t.Fatalfor setup failures- Interfaces belong to consumers, not producers
T.ContextandT.Chdir(Go 1.24+)b.Loop()(Go 1.24+): cleaner benchmarks, nob.ResetTimer()neededsynctest.Test(Go 1.25+): deterministic concurrent testing with synthetic time
Resource Management (Go 1.24+)
runtime.AddCleanup: multiple cleanups per object, no cycle leaks (replacesSetFinalizer)weak.Pointer[T]: weak references for caches, canonicalization, observersos.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 inmain //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-actionfor GitHub Actions - Suppress sparingly: prefer fixing over
//nolintcomments
Project Layout
Full guide:
skills/go-project-layout/SKILL.md
cmd/: one subdirectory per executable, keepmain.gothininternal/: 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
Secrettype with redactedString() - Standard crypto only:
crypto/randfor random bytes,bcryptfor passwords - HTTP timeouts: always set
ReadTimeout,WriteTimeout,IdleTimeout govulncheck: scan dependencies for known vulnerabilitiesgo test -race: always run with the race detector in CI
Experimental (Go 1.25)
encoding/json/v2: enable withGOEXPERIMENT=jsonv2. Better performance, streaming, custom marshalers per call.
Documentation
- Comments are full sentences starting with the declared name
- Package comments: before
packagedeclaration, no blank line