Go stdlib, error wrapping, interfaces, goroutines, table-driven tests, gofumpt.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add irahardianto/antigravity-setup --skill go-idioms --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Go Idioms?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/irahardianto-go-idioms)More formats (shields.io, HTML) on the badges page.
---
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:**
```go
module github.com/user/project
go 1.24.0
toolchain go1.24.0
```
---
### 4. Error Handling
1. **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)
2. **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
```go
return fmt.Errorf("creating task for user %s: %w", userID, err)
```
❌ Loses the error chain
```go
return fmt.Errorf("creating task: %v", err)
```
3. **Error string formatting**
- Error strings should be lowercase and have no trailing punctuation.
- Go convention from Code Review Comments.
✅ Correct formatting
```go
return fmt.Errorf("failed to open file %q: %w", path, err)
```
❌ Incorrect formatting
```go
return fmt.Errorf("Failed to open file: %w.", err)
```
4. **Use `errors.Join` for multi-error aggregation (Go 1.20+)**
```go
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...)
```
5. **Use sentinel errors for expected branch conditions**
```go
// 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
}
```
6. **Use typed errors for rich domain errors**
```go
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
}
```
7. **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.
8. **Use lazy evaluation for fallback values**
- Prefer creating errors lazily only when a failure occurs to avoid unnecessary allocations.
---
### 5. Interfaces
1. **Keep interfaces small — one or two methods is ideal**
✅ Focused, composable
```go
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
```
❌ Monolithic
```go
type FileManager interface {
Read()
Write()
Delete()
List()
Stat()
}
```
2. **"Accept interfaces, return structs"**
- Function parameters: accept interfaces for flexibility and testability
- Return values: return concrete structs so callers can access all methods
3. **Define interfaces where they are *used*, not where they are *implemented***
✅ Defined in the consumer package (task feature)
```go
// task/storage.go
type Storage interface {
GetByID(ctx context.Context, id string) (*Task, error)
}
// postgres.go implements Storage — it does NOT define it
```
4. **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
5. **`any` vs `interface{}`**
- Always use `any` (Go 1.18+ alias) instead of `interface{}`.
6. **Interface compliance verification**
- Use `var _ Interface = (*Struct)(nil)` to guarantee a type implements an interface at compile time.
```go
var _ Storage = (*PostgresStorage)(nil)
```
7. **When NOT to use interfaces**
- Premature abstraction violates YAGNI. Do not create an interface until you have 3+ implementations (or you need to mock it for testing in test-driven design).
8. **Generic interfaces with type parameters (Go 1.18+)**
```go
type Repository[T any] interface {
FindByID(ctx context.Context, id string) (T, error)
}
```
---
### 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.
1. **Always pass `context.Context` as the first parameter**
✅ Pass context
```go
func (s *Service) GetTask(ctx context.Context, id string) (*Task, error)
```
❌ No way to cancel or propagate deadlines
```go
func (s *Service) GetTask(id string) (*Task, error)
```
2. **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
go func() {
for {
select {
case <-ctx.Done():
return
case item := <-ch:
process(item)
}
}
}()
```
3. **Goroutine lifecycle management with `sync.WaitGroup`**
```go
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
go func(t Task) {
defer wg.Done()
process(t)
}(task)
}
wg.Wait()
```
4. **Use `errgroup` for concurrent fan-out with error collection**
```go
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 { ... }
```
5. **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"
6. **Close channels from the sender, never the receiver**
7. **`context.AfterFunc` (Go 1.21+)**
- Trigger an action asynchronously when a context completes.
```go
stop := context.AfterFunc(ctx, func() {
cleanupResources()
})
defer stop()
```
8. **Channel direction restrictions in function signatures**
- Narrow channel directions for compile-time safety.
```go
func producer(ch chan<- int) {} // Send-only
func consumer(ch <-chan int) {} // Receive-only
```
9. **`sync.OnceFunc` / `sync.OnceValue` (Go 1.21+)**
- Use for lazy, thread-safe initialization.
```go
var initDb = sync.OnceValue(func() *sql.DB {
// initialize DB
return db
})
```
10. **`select` with `default` for non-blocking operations**
```go
select {
case ch <- data:
// Sent
default:
// Channel is full or unbuffered with no receiver, move on
}
```
11. **`context.WithoutCancel` (Go 1.21+)**
- Create a derived context that is not cancelled when the parent is. Use for background operations that must outlive the request (e.g., async audit logging, metrics flush).
```go
// Request-scoped context cancelled when handler returns
bgCtx := context.WithoutCancel(ctx)
go func() {
// This work continues even after the parent request completes
auditLog(bgCtx, event)
}()
```
⚠️ Use sparingly — most work should respect parent cancellation. Reserve for fire-and-forget operations where partial completion is acceptable.
12. **Structured concurrency with `conc` (when `errgroup` isn't enough)**
- `errgroup` covers fan-out with error collection. For additional needs — panic recovery within goroutines, bounded worker pools with panic safety — use `github.com/sourcegraph/conc`.
```go
// conc.WaitGroup recovers panics and re-panics on Wait()
wg := conc.NewWaitGroup()
wg.Go(func() { process(item1) })
wg.Go(func() { process(item2) })
wg.Wait() // Re-panics if any goroutine panicked
```
- Prefer `errgroup` for simple fan-out. Reach for `conc` when you need panic recovery or bounded concurrency with `conc/pool`.
---
### 7. Naming Conventions
1. **Receiver names: short, consistent, and the first letter of the type**
✅
```go
func (s *Service) Create(...) {}
```
❌
```go
func (svc *Service) Create(...) {} // too verbose
func (self *Service) Create(...) {} // not Go
```
2. **Package names: short, lowercase, no underscores, no plurals**
✅ `package task`
❌ `package tasks` (plural)
❌ `package task_service` (underscore)
3. **Acronyms follow Go conventions (all caps or all lowercase)**
✅ `userID`, `HTTPClient`
❌ `userId`, `HttpClient`
4. **Unexported identifiers omit the type name** — if it's private, keep it terse
5. **Don't stutter** — `task.Task` is fine; `task.TaskService` is not
6. **Error string formatting**
- Error strings should not be capitalized or end with punctuation.
7. **Getter naming**
- No `Get` prefix in Go. Use `user.Name()` not `user.GetName()`.
8. **Test naming**
- Use the `TestFunctionName_Scenario_Expected` pattern.
```go
func TestCalculateDiscount_NegativeInput_ReturnsError(t *testing.T) { ... }
```
9. **Constructor naming**
- Use `New` prefix (`NewService`, `NewClient`).
10. **Interface naming**
- Use the `-er` suffix for single-method interfaces (`Reader`, `Writer`, `Stringer`).
---
### 8. Idiomatic Patterns
1. **Range-over-func iterators (Go 1.23+)**
```go
// Definition
func Count(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) {
return
}
}
}
}
// Usage
for i := range Count(5) {
fmt.Println(i)
}
```
2. **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.
3. **Type assertions with comma-ok pattern**
```go
if str, ok := val.(string); ok {
fmt.Println(str)
}
```
4. **Type switches for polymorphic dispatch**
```go
switch v := val.(type) {
case string:
fmt.Println("String:", v)
case int:
fmt.Println("Int:", v)
default:
fmt.Println("Unknown type")
}
```
5. **`strings.Builder` for string concatenation**
- Avoid `+` inside loops. Use `strings.Builder`.
```go
var b strings.Builder
b.WriteString("hello")
b.WriteString("world")
return b.String()
```
6. **`slices` and `maps` stdlib packages (Go 1.21+)**
- Use the built-in generic functions for sorting, reversing, filtering, and checking elements.
```go
slices.Sort(mySlice)
if slices.Contains(mySlice, target) { ... }
```
7. **Variadic append**
- Use `append(s1, s2...)` instead of loops.
8. **Pre-allocation**
- Use `make([]T, 0, n)` when the size is known to avoid reallocation overhead.
9. **`sync.Pool` for hot-path allocations**
- Use only for POINTER values to reduce GC pressure.
10. **`time.NewTicker` vs `time.Tick`**
- `time.Tick` leaks the underlying ticker. Always prefer `time.NewTicker` which can be stopped via `ticker.Stop()`.
11. **Struct embedding for composition (not inheritance)**
- Use struct embedding to reuse fields and methods where it forms an "is-a" relationship, not for general code reuse.
12. **Early returns / guard clauses**
- Reduce nesting.
```go
if err != nil {
return err
}
// Continue happy path
```
13. **Keep function complexity low**
- Keep cyclomatic complexity under 10 (matching Rust/TS guidelines).
14. **Functional options for optional configuration**
```go
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
}
```
15. **`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
```go
defer rows.Close()
```
✅ ALWAYS: Error-checked closure with structured logging
```go
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
```go
defer tx.Rollback()
```
✅ ALWAYS: Guard against sql.ErrTxDone (already committed)
```go
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
```go
defer resp.Body.Close()
```
✅ ALWAYS: Drain then close (prevents connection reuse issues)
```go
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)
}
}()
```
16. **Avoid `init()` functions**
- They run implicitly and make testing harder; prefer explicit initialization in `main` or constructors.
17. **Use named return values only for documentation or `defer`-based cleanup**
- Never rely on naked returns in non-trivial functions.
18. **`unique.Handle` for string interning (Go 1.23+)**
- Deduplicate frequently repeated values (user IDs, status strings) to reduce memory.
```go
import "unique"
h := unique.Make("active")
// h.Value() returns "active"
// Two handles with the same underlying value compare as equal
h1 := unique.Make("active")
fmt.Println(h == h1) // true — same canonical representation
```
- 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.
1. **Table-driven tests are the default pattern**
```go
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)
})
}
}
```
2. **Use `testify` for assertions**
- `require` for fatal assertions, `assert` for non-fatal.
3. **Run with the race detector in CI**
- `go test -race ./...`
4. **Use `httptest.NewRecorder()` for HTTP handler tests**
- No live server needed.
5. **Test behaviour, not implementation**
- Assert on outputs and side effects, not internal field values.
6. **Test helpers with `t.Helper()` and `testing.TB` interface**
```go
func createTestUser(t testing.TB) *User {
t.Helper() // Corrects line numbers in failure output
// ...
}
```
7. **`t.Cleanup()` for resource teardown**
- Automatically cleans up resources without needing defers across multiple helper functions.
```go
func setupDB(t *testing.T) *sql.DB {
db := connect()
t.Cleanup(func() { db.Close() })
return db
}
```
8. **Fuzz testing with `testing.F` (Go 1.18+)**
```go
func FuzzParse(f *testing.F) {
f.Add("valid_input")
f.Fuzz(func(t *testing.T, input string) {
Parse(input) // ensure it never panics
})
}
```
9. **Benchmark patterns with `testing.B`**
```go
func BenchmarkProcess(b *testing.B) {
b.ReportAllocs() // ALWAYS report allocations
b.ResetTimer() // Ignore setup time
for i := 0; i < b.N; i++ {
Process()
}
}
```
10. **Golden file testing**
- For large, complex outputs (like JSON or HTML), use golden files and compare against them.
11. **Test coverage non-negotiable policy**
- Every new exported function/method MUST have tests.
- Every branch MUST be exercised.
12. **Coverage commands**
- Run `go test -cover ./...` and `go test -coverprofile=coverage.out ./...`.
13. **Test Double Selection Table**
| 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
1. **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.
2. **Example functions**
- Use `func ExampleFunction()` with `// Output:` comments at the end of the function body for runnable examples in Godoc.
```go
func ExampleHello() {
fmt.Println("Hello")
// Output: Hello
}
```
3. **Package doc**
- For complex packages, place package documentation in a dedicated `doc.go` file.
4. **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
1. **Minimize dependency count**
- Go stdlib is highly comprehensive. Rely on it heavily before bringing in external packages.
2. **Commit `go.sum` always**
- Both `go.mod` and `go.sum` belong in version control to guarantee reproducible builds.
3. **`go mod tidy` before commits**
- Always run this to prune unused dependencies and resolve checksums.
4. **`govulncheck` in CI**
- Continuously scan for vulnerabilities in your module dependency tree.
5. **Module proxy and checksum database**
- Use `GOPROXY` and the checksum database. For private modules, configure `GOPRIVATE` or `GONOSUMCHECK`.
6. **Prefer stdlib over third-party**
- When feature parity exists (e.g., `log/slog` vs `zap`), use the stdlib.
---
### 13. Configuration and Environment
1. **Never scatter `os.Getenv()` calls throughout codebase**
- It makes dependencies opaque and testing difficult.
2. **Centralized config struct parsed at startup**
- Parse all configurations into a central `Config` struct once, then inject it (or its subsets) where needed.
3. **Fail fast**
- Fail at boot for missing required configuration, not later at the first time of use.
4. **Example with struct tags and validation**
```go
type Config struct {
Port int `env:"PORT" envDefault:"8080"`
Database string `env:"DATABASE_URL" envRequired:"true"`
}
func LoadConfig() (*Config, error) {
// load and validate
}
```
---
### 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
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!