Go Idioms and Patterns
Go favors simplicity, explicitness, readability. Intentionally small language — resist importing patterns from other languages. Boring and obvious = idiomatic.
Scope: Go-specific coding idioms. File layout:
@.gemini/skills/project-structure-go/SKILL.md. Test naming: GEMINI.md § Testing Strategy. Logging:@.gemini/skills/logging-and-observability-principles/SKILL.md.
Error Handling
Always return errors — never panic in library/business code.
panicreserved for unrecoverable states.recoveronly at top-level goroutine boundaries.Wrap with
%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)Sentinel errors for expected branches:
var ErrNotFound = errors.New("not found") var ErrUnauthorized = errors.New("unauthorized") if errors.Is(err, ErrNotFound) { // handle }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) } var ve *ValidationError if errors.As(err, &ve) { // access ve.Field, ve.Message }Handle at right level — propagate until enough context to act. Never swallow or re-wrap same error twice.
Interfaces
Small — one or two methods 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" — params accept interfaces for flexibility, return concrete structs.
Define where used, not where implemented:
// ✅ Defined in consumer package (task feature) // task/storage.go type Storage interface { GetByID(ctx context.Context, id string) (*Task, error) } // postgres.go implements Storage — does NOT define itImplicit satisfaction — no
implementskeyword needed. Any matching method set satisfies automatically.
Goroutines and Channels
General concurrency:
@.gemini/skills/concurrency-and-threading-principles/SKILL.md. Go-specific mechanics below.
Always pass
context.Contextfirst:// ✅ func (s *Service) GetTask(ctx context.Context, id string) (*Task, error) // ❌ — no cancellation/deadline propagation func (s *Service) GetTask(id string) (*Task, error)Never start goroutine without knowing how it stops:
go func() { for { select { case <-ctx.Done(): return case item := <-ch: process(item) } } }()errgroupfor concurrent fan-out:g, ctx := errgroup.WithContext(ctx) g.Go(func() error { return fetchUsers(ctx) }) g.Go(func() error { return fetchOrders(ctx) }) if err := g.Wait(); err != nil { ... }Channels for ownership transfer; mutexes for shared state. Close channels from sender only.
Naming
- Receivers: short, first letter of type.
(s *Service)not(svc *Service)or(self *Service). - Packages: short, lowercase, no underscores, no plurals.
package tasknotpackage tasks. - Acronyms: all caps or all lowercase.
userIDnotuserId.HTTPClientnotHttpClient. - Unexported: omit type name (it's private, keep terse).
- No stutter:
task.Taskfine;task.TaskServicenot.
Idiomatic Patterns
Functional options:
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 }defer— always error-checked closures. Never baredefer X.Close().// ❌ 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 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 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()— implicit, hard to test. Prefer explicit init inmainor constructors.Struct embedding for code reuse only when true "is-a" relationship.
Named return values only for documentation or defer cleanup. Never naked returns in non-trivial fns.
Testing
Test naming, pyramid: GEMINI.md § Testing Strategy. Go-specific tooling below.
Table-driven tests (default):
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) }) } }testify—require(fatal) +assert(non-fatal).Race detector in CI —
go test -race ./...httptest.NewRecorder()for handler tests. No live server.Test behavior, not implementation — assert outputs/side effects, not internal fields.
Formatting and Static Analysis
Must pass zero warnings/errors before commit. See GEMINI.md § Code Completion Mandate.
| 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 linter without comment explaining why.
//nolint:errcheckNEVER acceptable. Handle errors — even in defer. Use error-checked closure. #1 audit finding source.- Other
//nolint:require rationale comment AND code review approval. - Dev iteration:
go vet ./...for type-checking; reservegolangci-lintfor pre-commit.
Never
fmt.Println/log.Printfin production. Uselog/slog(Go 1.21+) or project adapter. See@.gemini/skills/logging-and-observability-principles/SKILL.md.
Related
- Code Idioms and Conventions GEMINI.md § Code Idioms and Conventions
- Project Structure — Go Backend @.gemini/skills/project-structure-go/SKILL.md
- Testing Strategy GEMINI.md § Testing Strategy
- Error Handling Principles GEMINI.md § Error Handling Principles
- Concurrency and Threading Principles @.gemini/skills/concurrency-and-threading-principles/SKILL.md
- Logging and Observability Principles @.gemini/skills/logging-and-observability-principles/SKILL.md
- Dependency Management Principles @.gemini/skills/dependency-management-principles/SKILL.md