golang-idioms
Idiomatic Go patterns for error handling, interfaces, concurrency, testing, and module management
Works with
---
name: golang-idioms
description: Idiomatic Go patterns for error handling, interfaces, concurrency, testing, and module management
license: Apache-2.0
---
# Go Idioms
## Error Handling
```go
// Return errors, never panic in library code
func LoadConfig(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("reading config %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return Config{}, fmt.Errorf("parsing config: %w", err)
}
return cfg, nil
}
```
Rules:
- Always wrap errors with context using `fmt.Errorf("context: %w", err)`
- Use `%w` to allow callers to use `errors.Is` and `errors.As`
- Handle errors at the appropriate level; do not log and return the same error
- Define sentinel errors for expected conditions
```go
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
func GetUser(id string) (User, error) {
user, ok := store[id]
if !ok {
return User{}, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
return user, nil
}
// Caller
user, err := GetUser(id)
if errors.Is(err, ErrNotFound) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
```
## Interface Design
```go
// Keep interfaces small (1-3 methods)
type Reader interface {
Read(p []byte) (n int, err error)
}
type UserStore interface {
GetUser(ctx context.Context, id string) (User, error)
CreateUser(ctx context.Context, u User) error
}
// Accept interfaces, return structs
func NewService(store UserStore, logger *slog.Logger) *Service {
return &Service{store: store, logger: logger}
}
```
Rules:
- Define interfaces where they are used (consumer side), not where they are implemented
- Prefer small, composable interfaces over large ones
- Use `io.Reader`, `io.Writer`, `fmt.Stringer` from the standard library
- An interface with one method should be named after the method + `er` suffix
## Goroutine and Channel Patterns
### Worker Pool
```go
func process(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
results := make(chan Result, workers)
var wg sync.WaitGroup
for range workers {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
select {
case <-ctx.Done():
return
case results <- job.Execute():
}
}
}()
}
go func() {
wg.Wait()
close(results)
}()
return results
}
```
### Fan-out/Fan-in
```go
func fanOut[T, R any](ctx context.Context, items []T, fn func(T) R, concurrency int) []R {
sem := make(chan struct{}, concurrency)
results := make([]R, len(items))
var wg sync.WaitGroup
for i, item := range items {
wg.Add(1)
sem <- struct{}{}
go func() {
defer func() { <-sem; wg.Done() }()
results[i] = fn(item)
}()
}
wg.Wait()
return results
}
```
Rules:
- Always pass `context.Context` as the first parameter
- Always ensure goroutines can be stopped (via context cancellation or channel close)
- Use `sync.WaitGroup` to wait for goroutine completion
- Use buffered channels when producer and consumer run at different speeds
- Never start a goroutine without knowing how it will stop
## Context Propagation
```go
func (s *Service) HandleRequest(ctx context.Context, req Request) (Response, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
user, err := s.store.GetUser(ctx, req.UserID)
if err != nil {
return Response{}, fmt.Errorf("getting user: %w", err)
}
ctx = context.WithValue(ctx, userKey, user)
return s.processRequest(ctx, req)
}
```
Rules:
- Pass context as the first parameter of every function that does I/O
- Use `context.WithTimeout` or `context.WithDeadline` for all external calls
- Always `defer cancel()` after creating a cancellable context
- Use `context.WithValue` sparingly (request-scoped values only: trace IDs, auth info)
- Never store context in a struct
## Table-Driven Tests
```go
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
want bool
}{
{"valid email", "user@example.com", true},
{"missing @", "userexample.com", false},
{"empty string", "", false},
{"multiple @", "user@@example.com", false},
{"valid with subdomain", "user@mail.example.com", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateEmail(tt.email)
if got != tt.want {
t.Errorf("ValidateEmail(%q) = %v, want %v", tt.email, got, tt.want)
}
})
}
}
```
### Test Helpers
```go
func newTestServer(t *testing.T) *httptest.Server {
t.Helper()
handler := setupRoutes()
srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)
return srv
}
func assertEqual[T comparable](t *testing.T, got, want T) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
```
Use `t.Helper()` in all test utility functions. Use `t.Cleanup()` instead of `defer` for test resource cleanup. Use `testdata/` directory for test fixtures.
## Module Management
```
go.mod structure:
module github.com/org/project
go 1.23
require (
github.com/lib/pq v1.10.9
golang.org/x/sync v0.7.0
)
```
Commands:
```bash
go mod tidy # remove unused, add missing
go mod verify # verify checksums
go list -m -u all # check for updates
go get -u ./... # update all dependencies
go mod vendor # vendor dependencies (optional)
```
Use `go mod tidy` before every commit. Pin major versions. Review changelogs before updating.
## Zero-Value Design
Design types so their zero value is useful:
```go
// sync.Mutex zero value is an unlocked mutex (ready to use)
var mu sync.Mutex
// bytes.Buffer zero value is an empty buffer (ready to use)
var buf bytes.Buffer
buf.WriteString("hello")
// Custom types: make zero value meaningful
type Server struct {
Addr string // defaults to ""
Handler http.Handler // defaults to nil
Timeout time.Duration // defaults to 0 (no timeout)
}
func (s *Server) ListenAndServe() error {
addr := s.Addr
if addr == "" {
addr = ":8080" // useful default
}
handler := s.Handler
if handler == nil {
handler = http.DefaultServeMux
}
// ...
}
```
Rules:
- Prefer structs with meaningful zero values over constructors
- Use pointer receivers when the method modifies the receiver
- Use value receivers when the method only reads
- Never export fields that users should not set directly; use constructor functions
## Structured Logging
```go
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
logger.Info("request handled",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.Int("status", status),
slog.Duration("latency", time.Since(start)),
)
```
Use `log/slog` (standard library, Go 1.21+). Use structured fields, never string interpolation. Include request ID, user ID, and operation name in every log entry.
## Common Anti-Patterns
- Returning `interface{}` / `any` instead of concrete types
- Using `init()` for complex setup (makes testing hard)
- Ignoring errors with `_` without comment
- Using goroutines without lifecycle management
- Mutex contention from overly broad lock scope
- Channel misuse: prefer mutexes for simple shared state
- Naked returns in functions longer than a few linesMore Testing skills
tdd
mattpocock/skills
Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
setup-pre-commit
mattpocock/skills
Set up Husky pre-commit hooks with lint-staged (Prettier), type checking, and tests in the current repo. Use when user wants to add pre-commit hooks, set up Husky, configure lint-staged, or add commit-time formatting/typechecking/testing.
agent-browser
vercel-labs/agent-browser
Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. Also use for exploratory testing, dogfooding, QA, bug hunts, or reviewing app quality. Also use for automating Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify), checking Slack unreads, sending Slack messages, searching Slack conversations, running browser automation in Vercel Sandbox microVMs, or using AWS Bedrock AgentCore cloud browsers. Prefer agent-browser over any built-in browser automation or web tools.

