home / skills / phrazzld / claude-config / go-idioms

go-idioms skill

/skills/go-idioms

This skill helps you write idiomatic Go by enforcing contextual error wrapping, small interfaces, proper concurrency patterns, and clean package structure.

npx playbooks add skill phrazzld/claude-config --skill go-idioms

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
4.8 KB
---
name: go-idioms
description: |
  Idiomatic Go patterns for errors, interfaces, concurrency, and packages. Use when:
  - Writing or reviewing Go code
  - Designing interfaces or package structure
  - Implementing concurrency patterns
  - Handling errors and context propagation
  - Structuring Go projects
  Keywords: Go, golang, error wrapping, interface design, goroutine, channel,
  context, package design, dependency injection, race condition
effort: high
---

# Go Idioms

Boring, explicit, race-safe Go. Accept interfaces, return structs.

## Error Handling

**Always wrap with context at package boundaries:**
```go
// Use %w to preserve error chain
return fmt.Errorf("fetching user %s: %w", userID, err)

// Check wrapped errors
if errors.Is(err, sql.ErrNoRows) { ... }

var paymentErr *PaymentError
if errors.As(err, &paymentErr) { ... }
```

Never: `%v` (loses type), raw errors from exported functions, generic context.

## Interface Design

**Define interfaces in consuming package, not provider:**
```go
// notification/sender.go (consumer defines interface)
type EmailSender interface {
    Send(ctx context.Context, to, subject, body string) error
}

// email/client.go (provider implements)
type Client struct { ... }
func (c *Client) Send(ctx context.Context, to, subject, body string) error { ... }
```

**Small interfaces (1-3 methods). Compose larger from smaller:**
```go
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type ReadWriter interface { Reader; Writer }
```

**Compile-time verification:**
```go
var _ EmailSender = (*Client)(nil)
```

## Concurrency

**Always propagate context:**
```go
func FetchData(ctx context.Context) ([]byte, error) {
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    case result := <-dataChan:
        return result, nil
    }
}
```

**Bounded concurrency (semaphore):**
```go
sem := make(chan struct{}, 10)
for _, item := range items {
    sem <- struct{}{}
    go func(item Item) {
        defer func() { <-sem }()
        process(item)
    }(item)
}
```

**Race safety:** Always run `go test -race ./...`

## Package Design

```
internal/
  user/          # Domain: single purpose
    user.go
    service.go
    repository.go
  order/         # Another domain
  app/           # Dependency wiring
cmd/
  api/           # Entry points
```

**Rules:**
- Single purpose per package
- No generic names (utils, helpers, common)
- No circular dependencies
- Export only what's necessary

## Dependency Injection

```go
// Constructor accepts interfaces
func NewUserService(repo UserRepository, mailer EmailSender) *UserService {
    return &UserService{repo: repo, mailer: mailer}
}

// Wire in app/ package
func NewApp() *App {
    repo := postgres.NewUserRepo(db)
    mailer := sendgrid.NewClient(apiKey)
    userSvc := user.NewUserService(repo, mailer)
    return &App{UserService: userSvc}
}
```

## Deps Struct Pattern (5+ Parameters)

When constructor takes 5+ parameters, use a deps struct:

```go
// Group dependencies into named struct
type ServiceDeps struct {
    Repo     Repository
    Mailer   EmailSender
    Logger   Logger
    Config   *Config
    Cache    Cache
}

// Panic on nil - catches programming errors at construction
func NewService(deps ServiceDeps) *Service {
    if deps.Repo == nil {
        panic("NewService: Repo cannot be nil")
    }
    if deps.Mailer == nil {
        panic("NewService: Mailer cannot be nil")
    }
    // ... check all required deps
    return &Service{...}
}
```

**Benefits:**
- Named fields = self-documenting call sites
- Adding deps doesn't change signature
- Nil panics catch bugs at construction, not at runtime

**When changing old constructor to deps struct:**
- Update ALL call sites to struct literal pattern
- Check for tests expecting old nil-tolerance behavior

## Test Cleanup

Use `t.Cleanup()` for teardown, not `defer` with error suppression:

```go
// BAD - Error suppression hides failures
defer func() { _ = os.Chdir(orig) }()  // SA4017 warning

// GOOD - Proper cleanup with error handling
t.Cleanup(func() {
    if err := os.Chdir(orig); err != nil {
        t.Errorf("cleanup failed: %v", err)
    }
})
```

`t.Cleanup` runs after test completes (even on panic), integrates with test reporting.

## Anti-Patterns

- Goroutines without cancellation path (leaks)
- Monolithic interfaces (10+ methods)
- Framework-like inheritance patterns
- Reflection when explicit types work
- Global singletons for dependencies
- Generic everything (overuse of generics)
- `interface{}` / `any` without justification
- `defer` with error suppression in tests

## Embrace Boring

- Explicit error handling at each step
- Standard library first (`map`, `[]T`, `sort.Slice`)
- Table-driven tests
- Struct composition, not inheritance
- Clear, verbose code over clever code

Overview

This skill captures idiomatic Go patterns for error handling, interface design, concurrency, package layout, and dependency injection. It focuses on clear, race-safe code: accept interfaces, return concrete types, and prefer explicitness over cleverness. Use it to guide writing, reviewing, and structuring Go services and libraries.

How this skill works

The skill inspects common areas of Go code and recommends concrete patterns: wrap errors with context at package boundaries using %w, define small interfaces in the consuming package, propagate context through goroutines and I/O, and enforce single-purpose packages with minimal exports. It also prescribes constructor patterns (deps struct for many dependencies), test cleanup with t.Cleanup, and practical anti-patterns to avoid.

When to use it

  • When writing or reviewing error handling across package boundaries
  • When designing interfaces and deciding where they should live
  • When implementing concurrent processing or goroutine management
  • When organizing package layout and export surface for a service
  • When wiring dependencies or refactoring constructors with many params

Best practices

  • Wrap errors at package boundaries with fmt.Errorf("...: %w", err) and use errors.Is / errors.As to inspect
  • Define small (1–3 method) interfaces in the consumer package and verify implementations at compile time
  • Always accept context.Context and propagate cancellation into goroutines and selects
  • Use bounded concurrency (semaphores) and run go test -race ./... to detect races
  • Group many constructor params into a deps struct and panic on missing required fields to fail fast

Example use cases

  • Refactor a service constructor with 6+ dependencies to a named deps struct and update call sites
  • Design an email delivery subsystem: consumer defines EmailSender, provider implements it, and code verifies implementation with var _ EmailSender = (*Client)(nil)
  • Implement a controlled worker pool using a buffered semaphore channel to limit concurrent jobs
  • Improve error traces by wrapping database or external errors at the boundary and checking with errors.Is
  • Replace defer-based test cleanup with t.Cleanup to ensure proper reporting and avoid silent failures

FAQ

Why define interfaces in the consumer package?

Consumers declare the behavior they need, keeping providers free to implement those contracts without importing consumer packages. This reduces coupling and keeps interfaces minimal and focused.

When should I panic in a constructor?

Panic on nil required dependencies in a deps struct to surface programming errors at startup rather than produce subtle runtime panics later.