# Go Best Practices

> Modern Go patterns — error handling, concurrency, structured logging with slog, and Go 1.22+ features agents miss

- Skill: `ofershap/go-best-practices` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ofershap/go-best-practices`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ofershap/go-best-practices/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ofershap (https://skillmd.com/u/ofershap)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ofershap/go-best-practices

---


## When to use

Use this skill when working with Go code. It teaches you the current best practices and prevents
common mistakes that AI agents make with outdated patterns.

## Critical Rules

### 1. Always wrap errors with context using fmt.Errorf and %w

**Wrong:**

```go
func createUser(name string) error {
    user, err := db.Insert(name)
    if err != nil {
        return err
    }
    return nil
}
```

**Correct:**

```go
func createUser(name string) error {
    user, err := db.Insert(name)
    if err != nil {
        return fmt.Errorf("creating user %s: %w", name, err)
    }
    return nil
}
```

**Why:** Error chains need context at each level for debugging; unwrapped errors lose origin
context.

### 2. Use log/slog for structured logging instead of log.Printf

**Wrong:**

```go
log.Printf("user %s created with id %d", name, id)
```

**Correct:**

```go
slog.Info("user created", "name", name, "id", id)
```

**Why:** slog is in the standard library since Go 1.21, produces structured (JSON/text) output,
supports levels.

### 3. Pass context.Context as first parameter, never store in structs

**Wrong:**

```go
type Service struct {
    ctx context.Context
}

func (s *Service) Do() error {
    return s.db.Query(s.ctx, "...")
}
```

**Correct:**

```go
type Service struct {
    db *sql.DB
}

func (s *Service) Do(ctx context.Context) error {
    return s.db.QueryContext(ctx, "...")
}
```

**Why:** Context has a lifecycle tied to the request, not the service; storing it conflates
lifecycles.

### 4. Prevent goroutine leaks — always ensure goroutines can exit

**Wrong:**

```go
go func() {
    for {
        processItem(<-ch)
    }
}()
```

**Correct:**

```go
go func() {
    for {
        select {
        case item, ok := <-ch:
            if !ok {
                return
            }
            processItem(item)
        case <-ctx.Done():
            return
        }
    }
}()
```

**Why:** Leaked goroutines consume memory (min 2KB stack each) and grow unboundedly.

### 5. Use errors.Is and errors.As for error checking, not == or type assertion

**Wrong:**

```go
if err == sql.ErrNoRows {
    return nil, nil
}
```

**Correct:**

```go
if errors.Is(err, sql.ErrNoRows) {
    return nil, nil
}
```

**Why:** errors.Is traverses the wrapped error chain; == only matches the outermost error.

### 6. Use range over integers (Go 1.22+) instead of C-style for loops

**Wrong:**

```go
for i := 0; i < 10; i++ {
    process(i)
}
```

**Correct:**

```go
for i := range 10 {
    process(i)
}
```

**Why:** Cleaner, less error-prone, idiomatic since Go 1.22.

### 7. Use table-driven tests with t.Run subtests

**Wrong:**

```go
func TestAdd_Positive(t *testing.T) { ... }
func TestAdd_Negative(t *testing.T) { ... }
func TestAdd_Zero(t *testing.T) { ... }
```

**Correct:**

```go
func TestAdd(t *testing.T) {
    tests := []struct {
        name  string
        a, b  int
        want  int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -2, -3},
        {"zero", 0, 5, 5},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}
```

**Why:** DRY, easy to add cases, subtests run independently and can be filtered.

### 8. Return concrete types from constructors, accept interfaces

**Wrong:**

```go
func NewService() ServiceInterface {
    return &service{}
}
```

**Correct:**

```go
func NewService() *Service {
    return &Service{}
}
```

**Why:** Accept interfaces, return structs — keeps packages decoupled, lets consumers define
interfaces they need.

### 9. Use errgroup for coordinated goroutine lifecycle

**Wrong:**

```go
var wg sync.WaitGroup
var mu sync.Mutex
var errs []error
for _, task := range tasks {
    wg.Add(1)
    go func(t Task) {
        defer wg.Done()
        if err := t.Run(); err != nil {
            mu.Lock()
            errs = append(errs, err)
            mu.Unlock()
        }
    }(task)
}
wg.Wait()
```

**Correct:**

```go
g, ctx := errgroup.WithContext(ctx)
for _, task := range tasks {
    task := task
    g.Go(func() error {
        return task.Run(ctx)
    })
}
if err := g.Wait(); err != nil {
    return fmt.Errorf("task failed: %w", err)
}
```

**Why:** errgroup handles sync, error collection, and context cancellation in one.

### 10. Avoid init() functions — use explicit initialization

**Wrong:**

```go
var db *sql.DB

func init() {
    db = connectDB()
}
```

**Correct:**

```go
func NewApp() (*App, error) {
    db, err := connectDB()
    if err != nil {
        return nil, fmt.Errorf("connect db: %w", err)
    }
    return &App{db: db}, nil
}
```

**Why:** init() creates hidden dependencies, makes testing difficult, order is unpredictable across
packages.

### 11. Handle the loop variable capture fix (Go 1.22+)

**Wrong (pre-1.22 pattern, unnecessary in 1.22+):**

```go
for _, item := range items {
    item := item
    go func() {
        process(item)
    }()
}
```

**Correct (Go 1.22+):**

```go
for _, item := range items {
    go func() {
        process(item)
    }()
}
```

**Why:** Go 1.22+ fixed loop variable capture; loop variables are per-iteration. Remove unnecessary
re-declarations.

### 12. Use proper struct validation, not manual checks

**Wrong:**

```go
func CreateUser(req *CreateUserRequest) error {
    if req.Name == "" {
        return errors.New("name required")
    }
    if len(req.Name) < 2 {
        return errors.New("name too short")
    }
    if req.Email == "" {
        return errors.New("email required")
    }
    // ...
}
```

**Correct:**

```go
type CreateUserRequest struct {
    Name  string `validate:"required,min=2"`
    Email string `validate:"required,email"`
}

func CreateUser(req *CreateUserRequest) error {
    if err := validator.Validate(req); err != nil {
        return fmt.Errorf("validation: %w", err)
    }
    // ...
}
```

**Why:** Declarative, consistent, handles complex validation rules.

## Patterns

### Functional options pattern for configurable constructors

```go
type Server struct {
    host string
    port int
}

type Option func(*Server)

func WithHost(host string) Option {
    return func(s *Server) { s.host = host }
}

func WithPort(port int) Option {
    return func(s *Server) { s.port = port }
}

func NewServer(opts ...Option) *Server {
    s := &Server{host: "localhost", port: 8080}
    for _, opt := range opts {
        opt(s)
    }
    return s
}
```

### Repository pattern with interface

```go
type UserRepository interface {
    GetByID(ctx context.Context, id int64) (*User, error)
    Create(ctx context.Context, u *User) error
}

type userRepo struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) *userRepo {
    return &userRepo{db: db}
}

func (r *userRepo) GetByID(ctx context.Context, id int64) (*User, error) {
    // ...
}
```

### Middleware chain pattern

```go
type Middleware func(http.Handler) http.Handler

func chain(middlewares ...Middleware) Middleware {
    return func(final http.Handler) http.Handler {
        for i := len(middlewares) - 1; i >= 0; i-- {
            final = middlewares[i](final)
        }
        return final
    }
}

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        slog.Info("request", "method", r.Method, "path", r.URL.Path)
        next.ServeHTTP(w, r)
    })
}
```

### Graceful shutdown with signal handling

```go
func main() {
    srv := &http.Server{Addr: ":8080", Handler: mux}
    go func() {
        if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            slog.Error("server failed", "err", err)
        }
    }()

    sig := make(chan os.Signal, 1)
    signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
    <-sig

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        slog.Error("shutdown failed", "err", err)
    }
}
```

## Anti-Patterns

- **Do not use panic for expected errors** — only for programmer bugs (nil dereference, out of
  bounds). Return errors for recoverable failures.

- **Do not ignore errors with `_ = someFunc()`** — handle or explicitly document why ignoring is
  acceptable.

- **Do not use global variables for dependency injection** — pass dependencies via constructors or
  function parameters.

- **Do not use naked goroutines without lifecycle management** — ensure every goroutine has an exit
  path (context cancellation, done channel, or bounded loop).

- **Do not return interfaces from packages** — return concrete types; let the consumer define the
  interface they need.

