Go Engineering
Reference files
This SKILL.md is a SUMMARY. The files under reference/ carry stricter rules
NOT all reproduced here. If your task touches a topic below, the reference is
required reading — don't trust the abbreviated version on this page.
reference/go-testing.md— table-driven tests, testify, subtests, helpersreference/go-aip.md— Google AIP and einride/aip-go for resource-oriented gRPCreference/error-handling.md— security-sensitive error patternsreference/logging.md— structuredslog, secret redaction, correlation IDsreference/linting.md— golangci-lint configurationreference/security-checklist.md— OWASP Go pre-deploy checklistreference/access-control.md,reference/authentication.md,reference/cryptography.md,reference/csrf.md,reference/database-security.md,reference/file-security.md,reference/input-validation.md,reference/session-management.md,reference/tls-https.md,reference/xss.md— security topics (OWASP)
API Inspection
Before guessing function signatures or fabricating package APIs, inspect the source:
go doc fmt.Errorf— package or symbol docs (stdlib + module deps)go doc -src strings.Cut— show sourcego doc ./internal/foo— local packagegopls definition <file>:<line>:<col>— jump to definitiongopls references <file>:<line>:<col>— find call sitesgopls symbols <file>— list package symbols
Use these over inferring APIs. Hallucinated signatures fail at compile time; go doc is faster than the build-fail loop.
Design Guidelines
- Keep things simple.
- Prefer stateless, pure functions over stateful structs with methods if no state is needed to solve the problem.
- Prefer synchronous code to concurrent code, an obvious concurrency pattern applies.
- Prefer simple APIs and small interfaces.
- Make the zero value useful:
bytes.Bufferandsync.Mutexwork without init. When zero values compose, there's less API. - Avoid external dependencies. A little copying is better than a little dependency.
- Clear is better than clever. Maintainability and readibility are important.
- Don't just check errors, handle them gracefully.
- Design the architecture, name the components, document the details: Good names carry the weight of expressing your design. If names are good, the design is clear on the page.
- Reduce nesting by using guard clauses.
- Invert conditions for early return
- In loops, use early
continuefor invalid items instead of nesting - Max 2-3 nesting levels
- Extract helpers for long methods
Code Style
Mustprefix for panicking functions- Enums: use
iota + 1to start at one, distinguishing intentional values from zero default
Error Handling
- Errors are values. Design APIs around that.
- Wrap with context using low-cardinality strings:
fmt.Errorf("get config: %w", err)rather thanfmt.Errorf("get config from %s: %w", path, err). APM tools that group by message text can collapse the same root cause across instances; high-cardinality wraps fragment the dashboard. - Variable data (IDs, names, paths) belongs in the structured log record at the application boundary (HTTP middleware, gRPC interceptor, top-level main), not at the wrap site. Intermediate layers wrap and return; the boundary observes the full chain and logs once with
slog.Error(..., "user_id", id, "error", err). - Caveat: if no boundary in your service preserves the variable as a structured field, embedding it in the wrap is the lesser evil — losing the data is worse than losing APM grouping. Treat low-cardinality as an optimization, not a correctness rule. Audit for
zap.String("user_id", id)/slog.Error(... "user_id", id ...)near the call boundary before stripping the variable from a wrap.// Bad: high-cardinality error string -- APM sees each user as a unique error return fmt.Errorf("fetch user %s: %w", userID, err) // Good: stable wrap; the boundary handler logs user_id structurally return fmt.Errorf("fetch user: %w", err) - Generic error matching:
errors.AsType[T](Go 1.26+) replaces thevar t *MyErr; errors.As(err, &t)dance - Join multiple errors:
errors.Join(err1, err2, err3)(Go 1.20+) - Avoid verb prefixes that accumulate through the stack: "failed to", "error", "could not", "unable to". Use bare context instead (
"connect: %w"not"failed to connect: %w","fetch config: %w"not"error fetching config: %w") - Always check errors immediately before using returned values — in Go 1.21–1.24, the compiler could reorder statements and execute method calls on nil receivers before the error check ran, causing panics (fixed in Go 1.25)
Error Strategy (Opaque Errors First)
Prefer opaque error handling: treat errors as opaque values, don't inspect internals. This minimizes coupling.
Three strategies in order of preference:
- Opaque errors (preferred): return and wrap errors without exposing type or value. Callers only check
err != nil. - Sentinel errors (
var ErrNotFound = errors.New(...)): use sparingly for expected conditions callers must distinguish. They become public API. - Error types (
type NotFoundError struct{...}): use when callers need structured context. Also public API — avoid when opaque or sentinel suffices.
Assert Behavior, Not Type
When you must inspect errors beyond errors.Is/errors.As, assert on behavior interfaces instead of concrete types:
type temporary interface {
Temporary() bool
}
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}
Handle Errors Once
An error should be handled exactly once. Handling = logging, returning, or degrading gracefully. Never log and return — duplicates without useful context.
// Bad: logs AND returns
if err != nil {
log.Printf("connect failed: %v", err)
return fmt.Errorf("connect: %w", err)
}
// Good: wrap (low-cardinality) and return; the boundary logs once
if err != nil {
return fmt.Errorf("connect: %w", err)
}
Wrap with context at each layer; log/handle only at the application boundary. The boundary picks up variable data (addr, user_id, etc.) from ctx-scoped log fields or from a structured slog.Error call there — not from each wrap site.
Context with Cause (Go 1.21+)
Propagate cancellation reasons through context:
ctx, cancel := context.WithCancelCause(parent)
cancel(fmt.Errorf("shutdown: %w", reason))
// Later retrieve the cause
if cause := context.Cause(ctx); cause != nil {
log.Printf("context cancelled: %v", cause)
}
Generics
- Constraints:
comparablefor map keys,cmp.Orderedfor sortable types - Custom constraints with type sets:
type Number interface { ~int | ~int64 | ~float64 } - Generic type alias (Go 1.24+):
type Set[T comparable] = map[T]struct{} - Self-referential constraints (Go 1.26+):
type Adder[A Adder[A]] interface { Add(A) A } - Prefer concrete types when generics add no value
Built-in Functions
min(a, b, c)andmax(a, b, c)- compute smallest/largest values (Go 1.21+)clear(m)- delete all map entries;clear(s)- zero all slice elements (Go 1.21+)new(expr)- allocate and initialize with value (Go 1.26+):ptr := new(computeValue())
Testing
See Go testing reference — covers table-driven tests, benchmarks (b.Loop(), Go 1.24+), testify assertion conventions (prefer require.Zero/Empty/Nil, ErrorIs, JSONEq), test doubles via nil-embedded interfaces, practices (TestFooBar naming, t.Helper/Cleanup/Context/Chdir/Parallel, -race, goleak for leak detection), and testing/synctest (Go 1.25+) for deterministic concurrent tests.
Concurrency
Share memory by communicating -- channels orchestrate; mutexes serialize.
Choosing a Synchronization Primitive
| Need | Use | Why |
|---|---|---|
| Single counter/flag | atomic |
Lock-free, simplest |
| Protect shared struct | sync.Mutex / sync.RWMutex |
Direct, no goroutine overhead |
| Transfer ownership of data | Unbuffered channel | Synchronizes sender and receiver |
| Fan-out/fan-in, pipelines | Buffered channel + select |
Composable, supports cancellation |
| N goroutines, first error aborts | errgroup.WithContext |
Propagates cancellation |
| N goroutines, collect all errors | errgroup + errors.Join |
No short-circuit |
| One-time init | sync.Once / sync.OnceValue |
Race-free lazy init |
| Deduplicate concurrent calls | singleflight.Group |
Coalesces in-flight requests |
sync.WaitGroup.Go()(Go 1.25+): combines Add(1) + goroutine launchvar wg sync.WaitGroup wg.Go(func() { work() }) // Combines Add(1) + go wg.Wait()- Make goroutine lifetime explicit; document when/why they exit
- Detect leaks with
GOEXPERIMENT=goroutineleakprofile(Go 1.26+) - Prefer synchronous functions; let callers add concurrency if needed
sync.Oncehelpers:sync.OnceFunc(),sync.OnceValue(),sync.OnceValues()(Go 1.21+)- Pointer receivers with mutexes (struct copy breaks lock semantics)
- Don't embed mutex (exposes Lock/Unlock to callers); use a named field instead
- Prevent copying of structs with mutexes or goroutine state using
noCopy:type Server struct { noCopy noCopy // go vet reports "copies lock value" if struct is copied mu sync.Mutex } type noCopy struct{} func (*noCopy) Lock() {} func (*noCopy) Unlock() {} - Channels: sender closes, receiver checks; never close from receiver side or with multiple concurrent senders
Channel Axioms
| Operation | nil channel | closed channel |
|---|---|---|
| Send | blocks forever | panics |
| Receive | blocks forever | returns zero value |
| Close | panics | panics |
- Nil channels are useful in
selectto disable a case - Use
for range chto receive until closed - Check closure with
v, ok := <-ch
Iterators (Go 1.22+)
Range Over Integers (Go 1.22+)
for i := range 10 {
fmt.Println(i) // 0..9
}
Range Over Functions (Go 1.23+)
// String iterators
for line := range strings.Lines(s) { }
for part := range strings.SplitSeq(s, sep) { }
for field := range strings.FieldsSeq(s) { }
// Slice iterators
for i, v := range slices.All(items) { }
for v := range slices.Values(items) { }
for v := range slices.Backward(items) { }
for chunk := range slices.Chunk(items, 3) { }
// Map iterators
for k, v := range maps.All(m) { }
for k := range maps.Keys(m) { }
for v := range maps.Values(m) { }
// Collect iterator to slice
keys := slices.Collect(maps.Keys(m))
sorted := slices.Sorted(maps.Keys(m))
// Custom iterator
func (s *Set[T]) All() iter.Seq[T] {
return func(yield func(T) bool) {
for v := range s.items {
if !yield(v) {
return
}
}
}
}
Interface Design
- Accept interfaces, return concrete types
- Define interfaces at the consumer, not the provider; keep them small (1-2 methods)
- Compile-time interface check:
var _ http.Handler = (*MyHandler)(nil) - For single-method dependencies, use function types instead of interfaces
- Don't embed types in exported structs (exposes methods and breaks API compatibility)
Slice & Map Patterns
- Pre-allocate when size known:
make([]User, 0, len(ids)) - Nil vs empty:
var t []string(nil, JSON null) vst := []string{}(non-nil, JSON[]) - Copy at boundaries with
slices.Clone(items)to prevent external mutations - Prefer
strings.Cut(s, "/")overstrings.Splitfor prefix/suffix extraction
Common Patterns
Options Pattern
Define type Option func(*Config). Create WithX functions returning Option that set fields. Constructor takes ...Option, applies each to default config.
Default Values (Go 1.22+)
Use cmp.Or(a, b, c) to return first non-zero value—e.g., cmp.Or(cfg.Port, envPort, 8080).
Context Usage
context.WithoutCancel(ctx)(Go 1.21+): derive a context that keeps values but ignores parent cancellation. Use for background work that must outlive the request (async cleanup, audit logging)
HTTP Best Practices
- Use
http.Server{}with explicitReadTimeout/WriteTimeout; avoidhttp.ListenAndServe - Method-based routing (Go 1.22+):
mux.HandleFunc("GET /items/{id}", h), extract viar.PathValue("id"). Greedy wildcard:{path...}
AIP: Resource-Oriented gRPC APIs
For building resource-oriented gRPC APIs following Google AIP standards with einride/aip-go, see AIP reference -- covers resource names, standard methods (CRUD), pagination, filtering, ordering, field masks, and field behavior annotations.
Security Helpers
- CSRF:
http.CrossOriginProtection(handler)(Go 1.25+) -- rejects cross-origin state-changing requests - Path traversal prevention:
os.OpenRoot("/var/data")(Go 1.24+) -- chroot-like scoped file access - GC cleanup:
runtime.AddCleanup(obj, fn)(Go 1.24+) -- replacesSetFinalizer, supports multiple cleanups per object
Structured Logging (log/slog)
- JSON handler for production:
slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) slog.ErrorContext(ctx, ...)overslog.Error(...)so handler-bound trace/request IDs propagate
Common Gotchas
Nil Safety
| Operation | nil map | nil slice | nil channel |
|---|---|---|---|
| Read/index | zero value | panics | blocks forever |
| Write | panics | panics (index) | blocks forever |
len / cap |
0 | 0 | 0 |
range |
0 iterations | 0 iterations | blocks forever |
append |
N/A | works (returns new) | N/A |
delete |
no-op | N/A | N/A |
close |
N/A | N/A | panics |
Don't add nil guards for values that a dependency (database, library, protocol) guarantees non-nil. Trust the contract; redundant checks add noise without safety. Only guard at true system boundaries (user input, external APIs, untrusted data).
Slice Aliasing
append on a sub-slice can silently mutate the original if capacity remains:
a := []int{1, 2, 3, 4}
b := a[:2] // b shares a's backing array
b = append(b, 99) // overwrites a[2]! a is now [1, 2, 99, 4]
Fix with full-slice expression to cap the capacity:
b := a[:2:2] // len=2, cap=2 -- append allocates a new array
b = append(b, 99) // a is unchanged
Copy Semantics
| Type | Assignment copies... |
|---|---|
| bool, int, float, complex, string | value (safe) |
| array | all elements (deep) |
| struct | all fields (shallow -- pointer fields share referent) |
| slice | header only (shares backing array) |
| map | header only (shares buckets) |
| pointer, func, channel | pointer (shares referent) |
| interface | header only (shares underlying value if pointer) |
Use slices.Clone / maps.Clone for shallow copies at API boundaries.
nilinterface vsnilconcrete:var err error = (*MyError)(nil)→err != nilis true (typed nil)- Loop variables: each iteration has own copy (Go 1.22+); older versions share
- Timer/Ticker channels: capacity 0 (Go 1.23+); previously capacity 1
Linting
Use golangci-lint with recommended linters: errcheck, govet, staticcheck, gocritic, gofumpt, wrapcheck, errorlint. See linting reference for the full .golangci.yml config template and commands. Projects with custom/private linters build a dedicated binary via the module plugin system (.custom-gcl.yml → golangci-lint custom) and must run that binary, not the one on PATH — see the same reference.
Pre-Commit
Check for Makefile targets first (make help, or read Makefile). Common targets:
make lintormake checkmake testmake build
Fallback if no Makefile:
go build ./...go test -v -race ./...golangci-lint rungo fix ./...(Go 1.26+: modernizes code to latest idioms)gofmt -w .orgoimports -w .go mod tidy
Performance
Profile-Guided Optimization (PGO)
# Collect profile
go test -cpuprofile=default.pgo
# PGO automatically enabled if default.pgo exists in main package
go build # Uses default.pgo
# Typical 2-7% performance improvement
Security
Based on OWASP Go Secure Coding Practices. See security checklist and guides for the full checklist, detailed per-topic guides (input validation, auth, crypto, sessions, TLS, CSRF, file security, XSS, access control, logging), and security scanning tools (gosec, govulncheck, trivy).
Source: gonzaloserrano/gopilot — distributed by TomeVault.