11 KiB
trigger, description
| trigger | description |
|---|---|
| model_decision | When writing Go code, reviewing Go idioms, or working on a Go backend project |
Go Idioms and Patterns
Core Philosophy
Go favors simplicity, explicitness, and readability. The language is intentionally small — resist the urge to import patterns from other languages. If it looks boring and obvious, it's probably idiomatic Go.
Scope: This file covers Go-specific coding idioms. For file layout, see
project-structure-go-backend.md. For test naming conventions, seetesting-strategy.md. For logging library choice, seelogging-and-observability-principles.md.
Error Handling
-
Always return errors — never panic in library or business code
panicis reserved for truly unrecoverable states (programmer errors, nil dereference)- Use
recoveronly at top-level goroutine boundaries (middleware, server startup)
-
Wrap errors with context using
%w// ✅ Preserves the error chain for errors.Is / errors.As return fmt.Errorf("creating task for user %s: %w", userID, err) // ❌ Loses the error chain return fmt.Errorf("creating task: %v", err) -
Use sentinel errors for expected branch conditions
// Define in errors.go var ErrNotFound = errors.New("not found") var ErrUnauthorized = errors.New("unauthorized") // Caller checks with errors.Is if errors.Is(err, ErrNotFound) { // handle } -
Use typed errors for rich domain errors
type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message) } // Caller unwraps with errors.As var ve *ValidationError if errors.As(err, &ve) { // access ve.Field, ve.Message } -
Handle errors at the right level — propagate upward until you have enough context to act on them; don't swallow or re-wrap the same error twice.
Interfaces
-
Keep interfaces small — one or two methods is ideal
// ✅ Focused, composable type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } // ❌ Monolithic type FileManager interface { Read(...); Write(...); Delete(...); List(...); Stat(...) } -
"Accept interfaces, return structs"
- Function parameters: accept interfaces for flexibility and testability
- Return values: return concrete structs so callers can access all methods
-
Define interfaces where they are used, not where they are implemented
// ✅ Defined in the consumer package (task feature) // task/storage.go type Storage interface { GetByID(ctx context.Context, id string) (*Task, error) } // postgres.go implements Storage — it does NOT define it -
Implicit satisfaction is a feature — don't use embedding to "implement" interfaces
- Any type with the right method set satisfies an interface automatically
- No
implementskeyword needed or wanted
Goroutines and Channels
For general concurrency principles (race conditions, deadlocks, message passing), see
concurrency-and-threading-principles.md. This section covers Go-specific mechanics.
-
Always pass
context.Contextas the first parameter// ✅ func (s *Service) GetTask(ctx context.Context, id string) (*Task, error) // ❌ — no way to cancel or propagate deadlines func (s *Service) GetTask(id string) (*Task, error) -
Never start a goroutine without knowing how it will stop
// ✅ Goroutine is bounded by context cancellation go func() { for { select { case <-ctx.Done(): return case item := <-ch: process(item) } } }() -
Use
errgroupfor concurrent fan-out with error collectiong, ctx := errgroup.WithContext(ctx) g.Go(func() error { return fetchUsers(ctx) }) g.Go(func() error { return fetchOrders(ctx) }) if err := g.Wait(); err != nil { ... } -
Prefer channels for ownership transfer; mutexes for shared state
- Channel: "I'm handing this data to you"
- Mutex: "We're both reading/writing this shared thing"
-
Close channels from the sender, never the receiver
Naming Conventions
-
Receiver names: short, consistent, and the first letter of the type
func (s *Service) Create(...) {} // ✅ func (svc *Service) Create(...) {} // ❌ — too verbose func (self *Service) Create(...) {} // ❌ — not Go -
Package names: short, lowercase, no underscores, no plurals
package task // ✅ package tasks // ❌ plural package task_service // ❌ underscore -
Acronyms follow Go conventions (all caps or all lowercase)
userID // ✅ userId // ❌ HTTPClient // ✅ HttpClient // ❌ -
Unexported identifiers omit the type name — if it's private, keep it terse
-
Don't stutter —
task.Taskis fine;task.TaskServiceis not
Idiomatic Patterns
-
Functional options for optional configuration
type Option func(*Service) func WithTimeout(d time.Duration) Option { return func(s *Service) { s.timeout = d } } func NewService(store Storage, opts ...Option) *Service { s := &Service{store: store, timeout: 30 * time.Second} for _, o := range opts { o(s) } return s } -
deferfor cleanup — always use error-checked closuresEvery deferred cleanup call that returns an error MUST check and log the error. Never use bare
defer X.Close()— the discarded error hides resource leak failures.// ❌ NEVER: Error silently discarded defer rows.Close() // ✅ ALWAYS: Error-checked closure with structured logging rows, err := db.QueryContext(ctx, query) if err != nil { return fmt.Errorf("querying tasks: %w", err) } defer func() { if err := rows.Close(); err != nil { slog.Warn("failed to close rows", "error", err, "operation", "ListTasks") } }()Transaction rollback:
// ❌ NEVER defer tx.Rollback() // ✅ ALWAYS: Guard against sql.ErrTxDone (already committed) defer func() { if err := tx.Rollback(); err != nil && !errors.Is(err, sql.ErrTxDone) { slog.Error("failed to rollback transaction", "error", err, "operation", "CreateOrder") } }()HTTP response body:
// ❌ NEVER defer resp.Body.Close() // ✅ ALWAYS: Drain then close (prevents connection reuse issues) defer func() { if _, err := io.Copy(io.Discard, resp.Body); err != nil { slog.Warn("failed to drain response body", "error", err) } if err := resp.Body.Close(); err != nil { slog.Warn("failed to close response body", "error", err) } }() -
Avoid
init()functions — they run implicitly and make testing harder; prefer explicit initialization inmainor constructors -
Prefer
structembedding over inheritance for code reuse, but only when the embedded type truly represents an "is-a" relationship -
Use named return values only for documentation or
defer-based cleanup — never rely on naked returns in non-trivial functions
Testing
Test file naming and pyramid proportions are defined in
testing-strategy.md. This section covers Go-specific tooling only.
-
Table-driven tests are the default pattern
func TestCalculateDiscount(t *testing.T) { tests := []struct { name string input float64 expected float64 wantErr bool }{ {"zero items", 0, 0, false}, {"negative input", -1, 0, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := calculateDiscount(tt.input) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) assert.Equal(t, tt.expected, got) }) } } -
Use
testifyfor assertions (requirefor fatal assertions,assertfor non-fatal) -
Run with the race detector in CI —
go test -race ./... -
Use
httptest.NewRecorder()for HTTP handler tests — no live server needed -
Test behaviour, not implementation — assert on outputs and side effects, not internal field values
Formatting and Static Analysis
All of the following must pass with zero warnings/errors before any commit. See code-completion-mandate.md for the full checklist.
| Tool | Purpose | Command |
|---|---|---|
gofumpt / goimports |
Canonical formatting | gofumpt -l -w . |
go vet |
Correctness checks | go vet ./... |
staticcheck |
Advanced static analysis | staticcheck ./... |
gosec |
Security scanning | gosec -quiet ./... |
golangci-lint |
Aggregated linter (CI) | golangci-lint run |
govulncheck |
Dependency CVE scanning | govulncheck ./... |
- Never disable a linter without a comment explaining why
//nolint:errcheckis NEVER acceptable. If a function returns an error, handle it — even indefer. Use an error-checked closure (see § Idiomatic Patterns above). This is the #1 source of audit findings.- Other
//nolint:directives require a// NOLINT:rationale comment AND must be approved during code review - Fast iteration during development:
go vet ./...type-checks and catches correctness issues without producing binaries (analogous tocargo check) — reservegolangci-lintfor pre-commit
Logging: Never use
fmt.Printlnorlog.Printfin production service code — these produce unstructured output. Uselog/slog(stdlib, Go 1.21+) or the project's chosen adapter. Seelogging-and-observability-principles.mdfor the required library and patterns.
Related Principles
- Code Idioms and Conventions @code-idioms-and-conventions.md
- Project Structure — Go Backend @project-structure-go-backend.md
- Testing Strategy @testing-strategy.md
- Error Handling Principles @error-handling-principles.md
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
- Logging and Observability Principles @logging-and-observability-principles.md
- Dependency Management Principles @dependency-management-principles.md