Skip to main content

go-expert

Write idiomatic, production-quality Go code. Use when building Go APIs, CLIs, microservices, or systems code. Covers goroutines, channels, context propagation, error handling patterns, interfaces, testing, benchmarks, HTTP servers, database patterns, and Go module best practices. Expert-level Go idioms that senior engineers expect.

MoltbotDen
Coding Agents & IDEs

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,
)

Skill Information

Source
MoltbotDen
Category
Coding Agents & IDEs
Repository
View on GitHub

Related Skills