Skip to main content
Coding Agents & IDEsDocumented

go-expert

Idiomatic production Go code. Goroutines, channels, error handling patterns, interfaces, context propagation, HTTP servers, pgx database patterns, testing, and structured logging. What senior Go engineers write.

Share:

Installation

npx clawhub@latest install go-expert

View the full skill documentation and source below.

Documentation

Go Production Expert

Go Philosophy

Go is explicit, not clever. Idiomatic Go is:

  • Simple over clever — readable in 6 months

  • Errors as values — handle them explicitly, every time

  • Composition over inheritance — small interfaces, embedding

  • Concurrency via CSP — communicate by sharing memory? No. Share memory by communicating.



Project Structure

myapp/
├── cmd/
│   ├── server/
│   │   └── main.go       # Entry point — thin, delegates to internal
│   └── worker/
│       └── main.go
├── internal/             # Private code — not importable by external packages
│   ├── api/
│   │   ├── handler.go
│   │   ├── handler_test.go
│   │   └── middleware.go
│   ├── domain/
│   │   ├── user.go
│   │   └── user_test.go
│   ├── repository/
│   │   ├── postgres.go
│   │   └── postgres_test.go
│   └── service/
│       └── user_service.go
├── pkg/                  # Public packages (reusable by external packages)
├── migrations/
├── go.mod
├── go.sum
└── Makefile

Error Handling

Rule 1: Always return errors, never ignore them.
Rule 2: Wrap errors with context at each layer.
Rule 3: Check errors.Is and errors.As, not string comparison.

// Define domain errors
type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with id %s not found", e.Resource, e.ID)
}

// Use sentinel errors for expected cases
var (
    ErrNotFound   = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrConflict   = errors.New("conflict")
)

// Wrap with context — %w makes it unwrappable
func (r *UserRepo) GetByID(ctx context.Context, id string) (*User, error) {
    var u User
    err := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id=$1", id).Scan(&u)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
    }
    if err != nil {
        return nil, fmt.Errorf("query user %s: %w", id, err)
    }
    return &u, nil
}

// Check wrapped errors
user, err := repo.GetByID(ctx, userID)
if errors.Is(err, ErrNotFound) {
    http.Error(w, "user not found", http.StatusNotFound)
    return
}
if err != nil {
    log.Error("failed to get user", "error", err, "user_id", userID)
    http.Error(w, "internal error", http.StatusInternalServerError)
    return
}

Interfaces (Small is Powerful)

// Define interfaces at the point of USE, not definition
// Small interfaces > large interfaces

// io.Reader is the gold standard — 1 method
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Your service interfaces
type UserStore interface {
    GetByID(ctx context.Context, id string) (*User, error)
    Create(ctx context.Context, u *User) error
    Update(ctx context.Context, u *User) error
    Delete(ctx context.Context, id string) error
}

// Compose for flexibility
type UserReader interface {
    GetByID(ctx context.Context, id string) (*User, error)
}

type UserWriter interface {
    Create(ctx context.Context, u *User) error
    Update(ctx context.Context, u *User) error
}

// Read-only service only needs UserReader
type ReadService struct {
    store UserReader  // NOT UserStore
}

// Test with a mock — no mocking library needed
type mockUserStore struct {
    users map[string]*User
}

func (m *mockUserStore) GetByID(_ context.Context, id string) (*User, error) {
    u, ok := m.users[id]
    if !ok {
        return nil, ErrNotFound
    }
    return u, nil
}

// Compile-time interface check
var _ UserStore = (*PostgresUserStore)(nil)

Context and Cancellation

// Always accept ctx as first parameter
func (s *Service) ProcessOrder(ctx context.Context, orderID string) error {
    // Propagate cancellation to all operations
    user, err := s.userStore.GetByID(ctx, orderID)
    if err != nil {
        return err
    }
    
    // Check for cancellation explicitly in long operations
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }
    
    return s.doExpensiveWork(ctx, user)
}

// Set deadlines — always
func handler(w http.ResponseWriter, r *http.Request) {
    // Add timeout to request context
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()  // ALWAYS defer cancel to prevent context leak
    
    result, err := service.Process(ctx, r.FormValue("id"))
    // ...
}

// Context values — only for request-scoped data
type contextKey string

const (
    RequestIDKey contextKey = "request_id"
    UserIDKey    contextKey = "user_id"
)

func WithRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, RequestIDKey, id)
}

func RequestIDFrom(ctx context.Context) string {
    id, _ := ctx.Value(RequestIDKey).(string)
    return id
}

Goroutines and Channels

// Goroutine lifecycle management with WaitGroup + errgroup
import "golang.org/x/sync/errgroup"

func ProcessBatch(ctx context.Context, items []Item) error {
    g, ctx := errgroup.WithContext(ctx)
    
    // Limit concurrency
    sem := make(chan struct{}, 10)  // 10 concurrent max
    
    for _, item := range items {
        item := item  // Capture loop variable (pre-Go 1.22)
        
        g.Go(func() error {
            sem <- struct{}{}
            defer func() { <-sem }()
            
            return processItem(ctx, item)
        })
    }
    
    return g.Wait()  // Returns first error, cancels context
}

// Fan-out/Fan-in pattern
func fanOut(ctx context.Context, input <-chan Work) []<-chan Result {
    numWorkers := 5
    channels := make([]<-chan Result, numWorkers)
    
    for i := range channels {
        channels[i] = worker(ctx, input)
    }
    return channels
}

func fanIn(ctx context.Context, channels ...<-chan Result) <-chan Result {
    var wg sync.WaitGroup
    merged := make(chan Result)
    
    output := func(c <-chan Result) {
        defer wg.Done()
        for r := range c {
            select {
            case merged <- r:
            case <-ctx.Done():
                return
            }
        }
    }
    
    wg.Add(len(channels))
    for _, c := range channels {
        go output(c)
    }
    
    go func() {
        wg.Wait()
        close(merged)
    }()
    
    return merged
}

// Channel direction typing (reduces bugs)
func producer(ch chan<- int) {  // Send only
    ch <- 42
}

func consumer(ch <-chan int) {  // Receive only
    v := <-ch
    fmt.Println(v)
}

HTTP Server

package main

import (
    "context"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    mux := http.NewServeMux()
    
    // Go 1.22+ pattern matching
    mux.HandleFunc("GET /api/users/{id}", handleGetUser)
    mux.HandleFunc("POST /api/users", handleCreateUser)
    mux.HandleFunc("GET /healthz", handleHealth)
    
    // Wrap with middleware chain
    handler := chain(
        mux,
        requestIDMiddleware,
        loggingMiddleware(logger),
        recoveryMiddleware,
    )
    
    server := &http.Server{
        Addr:         ":8080",
        Handler:      handler,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
        ErrorLog:     slog.NewLogLogger(logger.Handler(), slog.LevelError),
    }
    
    // Graceful shutdown
    go func() {
        logger.Info("starting server", "addr", server.Addr)
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            logger.Error("server error", "error", err)
            os.Exit(1)
        }
    }()
    
    // Wait for shutdown signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    logger.Info("shutting down...")
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := server.Shutdown(ctx); err != nil {
        logger.Error("shutdown error", "error", err)
    }
}

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

// Request ID middleware
func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := r.Header.Get("X-Request-ID")
        if id == "" {
            id = uuid.New().String()
        }
        ctx := WithRequestID(r.Context(), id)
        w.Header().Set("X-Request-ID", id)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Recovery middleware
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                slog.Error("panic", "recover", rec, "stack", debug.Stack())
                http.Error(w, "internal server error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

Database Patterns (pgx)

import (
    "github.com/jackc/pgx/v5/pgxpool"
    "github.com/jackc/pgx/v5"
)

type DB struct {
    pool *pgxpool.Pool
}

func NewDB(ctx context.Context, dsn string) (*DB, error) {
    config, err := pgxpool.ParseConfig(dsn)
    if err != nil {
        return nil, fmt.Errorf("parse db config: %w", err)
    }
    
    config.MaxConns = 25
    config.MinConns = 5
    config.MaxConnLifetime = time.Hour
    config.MaxConnIdleTime = 30 * time.Minute
    
    pool, err := pgxpool.NewWithConfig(ctx, config)
    if err != nil {
        return nil, fmt.Errorf("create pool: %w", err)
    }
    
    if err := pool.Ping(ctx); err != nil {
        return nil, fmt.Errorf("ping db: %w", err)
    }
    
    return &DB{pool: pool}, nil
}

// Transaction helper
func (db *DB) WithTx(ctx context.Context, fn func(pgx.Tx) error) error {
    tx, err := db.pool.Begin(ctx)
    if err != nil {
        return fmt.Errorf("begin tx: %w", err)
    }
    
    if err := fn(tx); err != nil {
        _ = tx.Rollback(ctx)
        return err
    }
    
    return tx.Commit(ctx)
}

// Usage
err = db.WithTx(ctx, func(tx pgx.Tx) error {
    _, err := tx.Exec(ctx, "UPDATE accounts SET balance=$1 WHERE id=$2", newBalance, accountID)
    if err != nil {
        return fmt.Errorf("update balance: %w", err)
    }
    
    _, err = tx.Exec(ctx, "INSERT INTO transactions (...) VALUES (...)", ...)
    return err
})

Testing

// table-driven tests
func TestGetUser(t *testing.T) {
    tests := []struct{
        name    string
        userID  string
        want    *User
        wantErr error
    }{
        {
            name:   "existing user",
            userID: "user-1",
            want:   &User{ID: "user-1", Name: "Alice"},
        },
        {
            name:    "missing user",
            userID:  "missing",
            wantErr: ErrNotFound,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // t.Parallel() // Add for independent tests
            
            store := &mockUserStore{
                users: map[string]*User{"user-1": {ID: "user-1", Name: "Alice"}},
            }
            svc := NewUserService(store)
            
            got, err := svc.GetUser(context.Background(), tt.userID)
            
            if !errors.Is(err, tt.wantErr) {
                t.Errorf("GetUser() error = %v, wantErr %v", err, tt.wantErr)
            }
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("GetUser() = %v, want %v", got, tt.want)
            }
        })
    }
}

// Benchmark
func BenchmarkProcessOrder(b *testing.B) {
    svc := newTestService()
    order := testOrder()
    
    b.ResetTimer()
    b.ReportAllocs()
    
    for i := 0; i < b.N; i++ {
        _ = svc.ProcessOrder(context.Background(), order)
    }
}

// Integration test with testcontainers
func TestPostgresRepo(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }
    
    ctx := context.Background()
    
    container, err := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:16"),
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
        testcontainers.WithWaitStrategy(
            wait.ForLog("database system is ready to accept connections"),
        ),
    )
    require.NoError(t, err)
    defer container.Terminate(ctx)
    
    dsn, _ := container.ConnectionString(ctx)
    db, err := NewDB(ctx, dsn)
    require.NoError(t, err)
    
    // Run migrations
    require.NoError(t, runMigrations(ctx, db))
    
    repo := NewUserRepo(db)
    // test against real database...
}

slog Structured Logging (Go 1.21+)

import "log/slog"

// Setup once
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
    AddSource: true,
}))
slog.SetDefault(logger)

// Use anywhere
slog.Info("user created", "user_id", userID, "email", email)
slog.Error("failed to process", "error", err, "order_id", orderID)

// With context
slog.InfoContext(ctx, "request processed",
    "request_id", RequestIDFrom(ctx),
    "duration_ms", time.Since(start).Milliseconds(),
    "status", statusCode,
)