| name | go-idioms |
| description | Go stdlib, error wrapping, interfaces, goroutines, table-driven tests, gofumpt. |
| paths | ["**/*.go","**/go.mod"] |
Go Idioms and Patterns
1. 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 references/project-structure.md. For detailed safety, SAST, and performance patterns, see references/go-patterns-and-anti-patterns.md. For logging library choice, see the logging-implementation skill. For quality commands, see the code-idioms-and-conventions rule.
Loading Guards: If the project has no go.mod, this skill does not apply.
2. When to Load References
| Situation | Reference to Load |
|---|
| Starting a new project or setting up file layout | references/project-structure.md |
Choosing packages, go.mod setup, or golangci-lint config | references/recommended-dependencies.md |
| Writing code that handles I/O, concurrency, or user input | references/go-patterns-and-anti-patterns.md |
3. Toolchain and Go Version
Default to the latest stable Go release. As of August 2026, Go 1.24 is the baseline with the Go module system.
Key version milestones:
- 1.24+ —
go tool for tool dependencies, swiss table map implementation
- 1.23+ —
range-over-func (iterator protocol), unique package
- 1.22+ —
range-over-integer, loop variable per-iteration scoping (eliminates loop variable capture bugs), enhanced ServeMux with method-based routing
- 1.21+ —
log/slog structured logging, maps and slices stdlib packages, sync.OnceFunc / sync.OnceValue / sync.OnceValues, min / max builtins, clear builtin
- 1.18+ — Generics, fuzz testing with
testing.F
Example go.mod configuration:
module github.com/user/project
go 1.24.0
toolchain go1.24.0
4. Error Handling
-
Always return errors — never panic in library or business code
panic is reserved for truly unrecoverable states (programmer errors, nil dereference)
- Use
recover only at top-level goroutine boundaries (middleware, server startup)
-
Wrap errors with context using %w
- Don't re-wrap at every level. Wrap once with meaningful context where it matters.
✅ 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)
-
Error string formatting
- Error strings should be lowercase and have no trailing punctuation.
- Go convention from Code Review Comments.
✅ Correct formatting
return fmt.Errorf("failed to open file %q: %w", path, err)
❌ Incorrect formatting
return fmt.Errorf("Failed to open file: %w.", err)
-
Use errors.Join for multi-error aggregation (Go 1.20+)
var errs []error
if err := doFirst(); err != nil {
errs = append(errs, err)
}
if err := doSecond(); err != nil {
errs = append(errs, err)
}
return errors.Join(errs...)
-
Use sentinel errors for expected branch conditions
var ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New()
errors.Is(err, ErrNotFound) {
}
5. 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)
type Storage interface {
GetByID(ctx context.Context, id string) (*Task, error)
}
-
Implicit satisfaction is a feature — don't use embedding to "implement" interfaces
- Any type with the right method set satisfies an interface automatically
- No
implements keyword needed or wanted
-
any vs interface{}
- Always use
any (Go 1.18+ alias) instead of interface{}.
-
Interface compliance verification
- Use
var _ Interface = (*Struct)(nil) to guarantee a type implements an interface at compile time.
var _ Storage = (*PostgresStorage)()
6. 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.Context as the first parameter
✅ Pass context
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 (Anti-pattern: Goroutine Leak)
- Every goroutine must have a definitive exit path (e.g., via
context.Context cancellation or channel close).
✅ Goroutine is bounded by context cancellation
go func() {
for {
select {
case <-ctx.Done():
return
case item := <-ch:
process(item)
}
}
}()
-
Goroutine lifecycle management with sync.WaitGroup
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
go func(t Task) {
defer wg.Done()
process(t)
}(task)
}
wg.Wait()
-
Use errgroup for concurrent fan-out with error collection
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { fetchUsers(ctx) })
g.Go( { fetchOrders(ctx) })
err := g.Wait(); err != { ... }
7. Naming Conventions
-
Receiver names: short, consistent, and the first letter of the type
✅
func (s *Service) Create(...) {}
❌
func (svc *Service) Create(...) {}
func (self *Service) Create(...) {}
-
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, HTTPClient
❌ userId, HttpClient
-
Unexported identifiers omit the type name — if it's private, keep it terse
-
Don't stutter — task.Task is fine; task.TaskService is not
-
Error string formatting
- Error strings should not be capitalized or end with punctuation.
-
Getter naming
- No
Get prefix in Go. Use user.Name() not user.GetName().
-
Test naming
- Use the
TestFunctionName_Scenario_Expected pattern.
func TestCalculateDiscount_NegativeInput_ReturnsError(t *testing.T) { ... }
-
Constructor naming
- Use
New prefix (NewService, NewClient).
-
8. Idiomatic Patterns
-
Range-over-func iterators (Go 1.23+)
func Count(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) {
return
}
}
}
}
for i := range Count(5) {
fmt.Println(i)
}
-
Nil slice vs empty slice semantics
var s []int (nil slice) marshals to null in JSON.
s := []int{} or make([]int, 0) (empty slice) marshals to [] in JSON.
-
Type assertions with comma-ok pattern
if str, ok := val.(string); ok {
fmt.Println(str)
}
-
Type switches for polymorphic dispatch
switch v := val.(type) {
case string:
fmt.Println("String:", v)
case int:
fmt.Println("Int:", v)
default:
fmt.Println("Unknown type")
}
-
strings.Builder for string concatenation
- Avoid
+ inside loops. Use .
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 for cleanup — always use error-checked closures
Every 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 in
main or constructors.
-
Use named return values only for documentation or defer-based cleanup
- Never rely on naked returns in non-trivial functions.
-
unique.Handle for string interning (Go 1.23+)
- Deduplicate frequently repeated values (user IDs, status strings) to reduce memory.
import "unique"
h := unique.Make("active")
h1 := unique.Make("active")
fmt.Println(h == h1)
- Use when you have many copies of the same string in memory (e.g., status enums parsed from JSON). Don't use for unique values — the interning overhead is wasted.
9. 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 testify for assertions
require for fatal assertions, assert for non-fatal.
-
Run with the race detector in CI
-
Use httptest.NewRecorder() for HTTP handler tests
-
Test behaviour, not implementation
- Assert on outputs and side effects, not internal field values.
-
Test helpers with t.Helper() and testing.TB interface
func createTestUser(t testing.TB) *User {
t.Helper()
}
| Approach | When to Use |
|---|
| Hand-written fake (implement interface) | Simple interface, few methods, need stateful behavior |
gomock/mockgen | Complex interface, need to verify call counts, argument matching |
testify/mock | Complex interface, prefer fluent assertion API |
| Table-driven tests | Same logic, multiple input/output pairs |
go-cmp | Deep equality comparison with custom options |
Prefer hand-written fakes for core domain traits.
10. Formatting and Static Analysis
All of the following must pass with zero warnings/errors before any commit. See code-idioms-and-conventions.md for the full checklist.
Feedback Loop — Development Workflow:
| Phase | Command | Purpose |
|---|
| TDD / rapid iteration | go vet ./... | Type-checks and correctness — fastest loop |
| Pre-commit | golangci-lint run | Aggregated linting — must pass zero warnings |
| Pre-commit | gofumpt -l -w . | Formatting — non-negotiable |
| Pre-commit | go test -race ./... | Unit tests with race detector |
| Coverage verification | go test -coverprofile=c.out ./... | Verify before merging |
| Security audit | govulncheck ./... | CVE scanning |
Lint suppression policy — NEVER suppress these:
| Lint | What It Signals | What To Do Instead |
|---|
errcheck | Unchecked error returns | Handle the error — use error-checked closure in defer |
govet shadow | Variable shadowing | Rename the inner variable |
staticcheck SA* | Correctness issues | Fix the underlying bug |
gosec G* (security) | Security vulnerability | Fix the vulnerability |
cyclop / gocognit | Function too complex | Decompose into smaller functions |
Acceptable suppressions (with mandatory // nolint: comment + rationale):
| Lint | When Acceptable |
|---|
gosec G104 (unhandled error) | In test code only, with // nolint:gosec // test-only |
revive exported | When the exported symbol is intentionally part of API design |
Rule of thumb: If you're about to write //nolint:, stop and ask if you're suppressing a real design problem.
//nolint:errcheck is NEVER acceptable. If a function returns an error, handle it — even in defer. Use an error-checked closure. This is the #1 source of audit findings.
Logging: Never use fmt.Println or log.Printf in production service code — these produce unstructured output. Use log/slog (stdlib, Go 1.21+) or the project's chosen adapter. See @.agents/skills/logging-implementation/SKILL.md for the required library and patterns.
11. Documentation
-
Godoc conventions
- Package-level doc comment should exist for all exported packages.
- All exported symbols (functions, structs, interfaces, vars, consts) MUST have a documentation comment starting with the name of the symbol.
-
Example functions
- Use
func ExampleFunction() with // Output: comments at the end of the function body for runnable examples in Godoc.
func ExampleHello() {
fmt.Println("Hello")
}
-
Package doc
- For complex packages, place package documentation in a dedicated
doc.go file.
-
Document the WHY, not the WHAT
- The code shows what is happening. Comments explain why it is done this way (design rationale, edge cases, workarounds).
12. Dependency Management
-
Minimize dependency count
- Go stdlib is highly comprehensive. Rely on it heavily before bringing in external packages.
-
Commit go.sum always
- Both
go.mod and go.sum belong in version control to guarantee reproducible builds.
-
go mod tidy before commits
- Always run this to prune unused dependencies and resolve checksums.
-
govulncheck in CI
- Continuously scan for vulnerabilities in your module dependency tree.
-
Module proxy and checksum database
- Use
GOPROXY and the checksum database. For private modules, configure GOPRIVATE or GONOSUMCHECK.
-
Prefer stdlib over third-party
- When feature parity exists (e.g.,
log/slog vs zap), use the stdlib.
13. Configuration and Environment
-
Never scatter os.Getenv() calls throughout codebase
- It makes dependencies opaque and testing difficult.
-
Centralized config struct parsed at startup
- Parse all configurations into a central
Config struct once, then inject it (or its subsets) where needed.
-
Fail fast
- Fail at boot for missing required configuration, not later at the first time of use.
-
Example with struct tags and validation
type Config struct {
Port int `env:"PORT" envDefault:"8080"`
Database string `env:"DATABASE_URL" envRequired:"true"`
}
func LoadConfig() (*Config, error) {
}
14. Safety, Security, and Performance
- Key safety rules (non-negotiable):
- Never ignore errors.
- Always close resources safely in a
defer.
- Validate all user input at the system boundary.
- For a full catalog: Point to
references/go-patterns-and-anti-patterns.md.
- For Go-specific profiling: Point to the
perf-optimization skill.
15. Related Principles
- Code Idioms and Conventions @code-idioms-and-conventions.md
- Project Structure — Go Backend @references/project-structure.md
- Security Principles @security-principles.md
- Architectural Patterns — Testability-First Design @architectural-pattern.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 Mandate @logging-and-observability-mandate.md
- Logging and Observability Principles @.agents/skills/logging-implementation/SKILL.md
- Dependency Management Principles @dependency-management-principles.md