Go Conventions
- Run
gofmt and goimports on every save — non-negotiable; eliminates all formatting debates
- Run
gosec for static security analysis in CI
- Run
go vet and staticcheck as part of the linting pipeline
- Use
golangci-lint to aggregate all linters in one pass
Structured Logging
- Use log/slog for all new code (Go 1.21+) — structured key-value logging in the standard library
- Use slog.With() to attach context (request ID, user ID) — avoids repeating fields in every log call
- Choose JSONHandler for production, TextHandler for development
Interfaces
- Accept interfaces, return structs — keep contracts flexible for callers, concrete for implementers
- Keep interfaces small: 1-3 methods maximum — large interfaces are harder to implement and mock
- Define interfaces where they are consumed, not where they are implemented
- Use the standard naming convention: single-method interfaces end in
-er
Error Handling
- Never ignore errors — handle or propagate every one
- Wrap errors with context using
fmt.Errorf and the %w verb
- Check errors immediately after the call — no deferred checking
- Use sentinel errors or custom types for errors callers must inspect
// BAD: error ignored
data, _ := os.ReadFile(path)
// GOOD: error wrapped with context
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read config %s: %w", path, err)
}
Generics
- Use generics for type-safe data structures and utility functions — avoid interface{}/any when the type is known at call sites
- Prefer concrete types when only one or two types are needed — generics add complexity
- Use type constraints from the constraints package or define your own with interface unions
- Avoid over-generalizing — write the concrete version first, generalize only when a second type appears
Concurrency
- Pass
context.Context as the first parameter to all external calls — enables cancellation and deadline propagation
- Set timeouts on every context used for network or database operations
- Use channels for communication, mutexes for state protection
- Never start goroutines without a plan to stop them — leaked goroutines cause memory leaks and deadlocks
Testing
- Write table-driven tests for functions with multiple input scenarios
- Run tests with
-race flag in CI to detect data races
- Use
t.Helper() in test helper functions for accurate line reporting
- Use
testify/assert or standard library — keep test dependencies minimal
func TestParsePort(t *testing.T) {
tests := []struct {
name string
input string
want int
wantErr bool
}{
{"valid port", "8080", 8080, false},
{"empty string", "", 0, true},
{"out of range", "99999", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParsePort(tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("error = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}
HTTP Routing (Go 1.22+)
- Use the standard net/http.ServeMux — Go 1.22+ supports method matching and path wildcards natively
- Pattern format: “METHOD /path/{param}” — e.g.,
mux.HandleFunc("GET /tasks/{id}", handler)
- Evaluate third-party routers only when you need middleware chaining or OpenAPI integration not covered by ServeMux
Iterators (Go 1.23+)
- Use range-over-function iterators for custom sequences — implement func(yield func(K, V) bool)
- Use slices.All, slices.Values, slices.Backward for standard iteration patterns over slices
- Use maps.Keys, maps.Values for map iteration
- Use
for i := range n to iterate from 0 to n-1 — cleaner than C-style for loops
Configuration
- Use the functional options pattern for configurable constructors
- Provide sensible defaults — require only what cannot be defaulted
- Load secrets from environment variables — never embed in source
Project Structure
- Keep
main.go minimal — delegate to internal packages
- Use
internal/ to prevent external imports of private packages — the Go compiler enforces this boundary
- Group by domain, not by layer (avoid generic
models/, utils/ packages)
- Prefer returning concrete types from constructors for discoverability