home / skills / phrazzld / claude-config / 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-idiomsReview the files below or copy the command above to add this skill to your agents.
---
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
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.
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.
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.