go-ops
Go development patterns, concurrency, error handling, testing, and project structure. Use for: golang, go, goroutine, channel, context, errgroup, go test, go mod, go build, interface, generics, table-driven tests, worker pool, sync.Mutex, sync.WaitGroup, pprof, go vet, golangci-l
Install
npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/go-ops
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
git clone https://github.com/0xDarkMatter/claude-mods.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Go Operations
Comprehensive Go skill covering idiomatic patterns, concurrency, and production practices.
Module Quick Start
# New module
go mod init github.com/user/project
# Add dependency
go get github.com/lib/pq@latest
# Tidy (remove unused, add missing)
go mod tidy
# Vendor dependencies
go mod vendor
# Workspace (multi-module)
go work init ./api ./shared
go work use ./cli
Error Handling Decision Tree
What kind of error?
│
├─ Known, expected condition (e.g. "not found")
│ └─ Sentinel error: var ErrNotFound = errors.New("not found")
│ └─ Caller checks: errors.Is(err, ErrNotFound)
│
├─ Need to carry structured data (status code, field name)
│ └─ Custom error type: type ValidationError struct { Field, Message string }
│ └─ Implement Error() string
│ └─ Caller checks: errors.As(err, &validErr)
│
├─ Adding context to an existing error
│ └─ Wrap: fmt.Errorf("load config: %w", err)
│ └─ Preserves original for Is/As checks
│
├─ Truly unrecoverable (corrupted state, programmer bug)
│ └─ panic("invariant violated: ...")
│ └─ Almost never in library code
│
└─ Multiple errors from concurrent work
└─ errors.Join(err1, err2) or multierr package
Error Wrapping Convention
// Add context at each layer, don't repeat the function name
func LoadUser(id int) (*User, error) {
row, err := db.Query("SELECT ...", id)
if err != nil {
return nil, fmt.Errorf("load user %d: %w", id, err)
}
// ...
}
Concurrency Decision Tree
What's the concurrency pattern?
│
├─ Run N independent tasks, collect results
│ └─ errgroup.Group (cancels on first error)
│
├─ Fire-and-forget background work
│ └─ go func() with context for cancellation
│ └─ ALWAYS handle the error or log it
│
├─ Producer/consumer pipeline
│ └─ Channels (buffered for throughput)
│ └─ Close channel when producer is done
│
├─ Rate-limited concurrent work
│ └─ Semaphore: make(chan struct{}, maxConcurrency)
│
├─ Shared mutable state
│ └─ sync.Mutex or sync.RWMutex
│ └─ Prefer channels if the state is simple
│
├─ One-time initialization
│ └─ sync.Once
│
└─ Wait for N goroutines to finish (no error collection)
└─ sync.WaitGroup
errgroup Quick Start
import "golang.org/x/sync/errgroup"
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // max 10 concurrent goroutines
for _, url := range urls {
g.Go(func() error {
return fetch(ctx, url)
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf("fetch urls: %w", err)
}
Deep dive: Load ./references/concurrency.md for worker pools, fan-out/fan-in, pipeline patterns, context best practices.
Interface Design
Accept interfaces, return structs.
// Good: function accepts interface
func Process(r io.Reader) error { ... }
// Good: return concrete type
func NewServer(cfg Config) *Server { ... }
// Bad: returning interface (hides implementation, prevents extension)
func NewServer(cfg Config) ServerInterface { ... }
Common Stdlib Interfaces
| Interface | Methods | Use For |
|---|---|---|
io.Reader |
Read([]byte) (int, error) |
Any byte source |
io.Writer |
Write([]byte) (int, error) |
Any byte sink |
io.Closer |
Close() error |
Resource cleanup |
fmt.Stringer |
String() string |
String representation |
error |
Error() string |
Error values |
sort.Interface |
Len, Less, Swap |
Custom sorting |
http.Handler |
ServeHTTP(w, r) |
HTTP handlers |
encoding.BinaryMarshaler |
MarshalBinary() ([]byte, error) |
Binary encoding |
Functional Options Pattern
type Option func(*Server)
func WithPort(port int) Option {
return func(s *Server) { s.port = port }
}
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func NewServer(opts ...Option) *Server {
s := &Server{port: 8080, timeout: 30 * time.Second} // defaults
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
srv := NewServer(WithPort(9090), WithTimeout(5*time.Second))
Deep dive: Load ./references/interfaces-generics.md for generics, type constraints, embedding, type assertions.
Testing Quick Reference
// Table-driven test
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 1, 2, 3},
{"zero", 0, 0, 0},
{"negative", -1, -2, -3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
# Run tests
go test ./...
# With coverage
go test -cover -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Run specific test
go test -run TestAdd ./pkg/math/
# Benchmarks
go test -bench=. -benchmem ./...
# Race detector
go test -race ./...
# Fuzz testing
go test -fuzz=FuzzParse ./...
Deep dive: Load ./references/testing.md for mocking with interfaces, httptest, testcontainers, golden files.
Common Gotchas
| Gotcha | Why | Fix |
|---|---|---|
| Nil slice vs empty slice | var s []int is nil, s := []int{} is empty. json.Marshal gives null vs [] |
Use make([]int, 0) or []int{} if JSON matters |
| Goroutine leak | Goroutine blocked on channel with no reader/writer | Use context.WithCancel, always provide exit path |
| Defer in loop | Deferred calls don't run until function returns | Wrap loop body in a closure or use explicit cleanup |
| Interface nil pitfall | (*MyType)(nil) assigned to error interface is not == nil |
Return nil explicitly, not a nil typed pointer |
| Range variable capture | Loop var reused (pre-Go 1.22) | Use go func(v T) { ... }(v) or upgrade to Go 1.22+ |
| String concatenation in loop | O(n^2) allocation | Use strings.Builder |
sync.WaitGroup Add after Go |
Race condition | Call wg.Add(1) before go func() |
| Unbuffered channel deadlock | Send/receive must happen concurrently | Use buffered channel or separate goroutines |
map not safe for concurrent use |
Race condition, may crash | Use sync.Mutex or sync.Map |
Project Structure
project/
├── cmd/
│ ├── api/main.go # Entry points
│ └── worker/main.go
├── internal/ # Private packages
│ ├── handler/
│ ├── service/
│ └── repository/
├── pkg/ # Public packages (optional)
├── go.mod
├── go.sum
├── Makefile # or justfile
└── .golangci.yml
Deep dive: Load ./references/project-structure.md for workspace mode, build tags, ldflags, linting config.
Performance Quick Reference
# CPU profile
go test -cpuprofile=cpu.prof -bench=. ./...
go tool pprof cpu.prof
# Memory profile
go test -memprofile=mem.prof -bench=. ./...
go tool pprof -alloc_space mem.prof
# Trace
go test -trace=trace.out ./...
go tool trace trace.out
# Escape analysis
go build -gcflags='-m' ./...
| Optimization | When | Pattern |
|---|---|---|
| Pre-allocate slices | Known size | make([]T, 0, n) |
strings.Builder |
String concatenation | var b strings.Builder |
sync.Pool |
Frequent alloc/free of same type | pool.Get() / pool.Put() |
| Struct field alignment | Memory-sensitive | Group fields by size (largest first) |
| Buffer reuse | I/O-heavy | bufio.NewReaderSize(r, 64*1024) |
Deep dive: Load ./references/performance.md for pprof walkthrough, benchmarking patterns, escape analysis.
Reference Files
Load these for deep-dive topics. Each is self-contained.
| Reference | When to Load |
|---|---|
./references/concurrency.md |
Goroutines, channels, context, sync primitives, worker pools, pipelines |
./references/error-handling.md |
Error wrapping, sentinel errors, custom types, multi-error, panic/recover |
./references/testing.md |
Table tests, mocking, httptest, benchmarks, fuzz, testcontainers, golden files |
./references/interfaces-generics.md |
Interface design, embedding, type assertions, generics, type constraints |
./references/project-structure.md |
Standard layout, go.mod, workspaces, build tags, ldflags, golangci-lint |
./references/performance.md |
pprof, trace, benchmarks, escape analysis, sync.Pool, struct alignment |
./references/expert-insights.md |
HTTP server (Go 1.22 routing), graceful shutdown, http.Client tuning, JSON helpers |
See Also
docker-ops- Multi-stage builds for Go binaries (scratch/distroless)ci-cd-ops- Go CI pipelines, caching go modules, goreleasertesting-ops- Cross-language testing strategies
Files (claude-mods)
-
assets
-
.gitkeep 0 B · in bundle
-
-
references
-
concurrency.md 18.8 KB
# Go Concurrency Reference ## Table of Contents 1. [Goroutines](#goroutines) 2. [Channels](#channels) 3. [Select](#select) 4. [Context](#context) 5. [Sync Primitives](#sync-primitives) 6. [errgroup](#errgroup) 7. [Worker Pool Pattern](#worker-pool-pattern) 8. [Fan-out / Fan-in](#fan-out--fan-in) 9. [Pipeline Pattern](#pipeline-pattern) 10. [Rate Limiting](#rate-limiting) 11. [Common Mistakes](#common-mistakes) --- ## Goroutines ### Launch Patterns ```go // Anonymous function - capture variables carefully go func() { doWork() }() // Named function go processItem(item) // Method go srv.handleConnection(conn) // Capture loop variable correctly (pre-Go 1.22) for _, item := range items { item := item // shadow to capture go func() { process(item) }() } // Go 1.22+: loop variable captured per iteration automatically for _, item := range items { go func() { process(item) // safe in Go 1.22+ }() } ``` ### Goroutine Lifecycle Every goroutine needs an exit path. Establish ownership at creation time. ```go func startWorker(ctx context.Context, jobs <-chan Job) { go func() { for { select { case <-ctx.Done(): return // clean exit on cancellation case job, ok := <-jobs: if !ok { return // channel closed } process(job) } } }() } ``` ### Avoid Goroutine Leaks A goroutine leaks when it blocks forever with no exit condition. ```go // LEAK: goroutine blocks on send forever if nobody reads func bad() { ch := make(chan int) go func() { ch <- compute() // blocks if caller exits }() // if caller returns without reading ch, goroutine leaks } // FIX: use buffered channel or context func good(ctx context.Context) { ch := make(chan int, 1) // buffer absorbs the send go func() { select { case ch <- compute(): case <-ctx.Done(): } }() } ``` ### Cost Model - Initial stack: ~2 KB (grows as needed, up to 1 GB by default) - Goroutines are multiplexed onto OS threads by the Go scheduler - Switching between goroutines is cheap (~100 ns) vs OS thread switch (~1 µs) - Practical limit: tens of thousands of goroutines; millions is unusual but possible - Use `runtime.NumGoroutine()` to inspect count; expose via `pprof` in production --- ## Channels ### Buffered vs Unbuffered ```go // Unbuffered: sender blocks until receiver is ready (synchronous handoff) ch := make(chan int) // Buffered: sender blocks only when buffer is full ch := make(chan int, 10) ``` Use unbuffered when you want a synchronization guarantee (the receiver got the value). Use buffered to decouple producer/consumer speeds or to avoid goroutine creation. ### Directional Channels ```go // Restrict channels at function boundaries for clarity and safety func produce(out chan<- int) { // send-only out <- 42 } func consume(in <-chan int) { // receive-only v := <-in fmt.Println(v) } func wire() { ch := make(chan int, 1) go produce(ch) consume(ch) } ``` ### Close Semantics ```go // Only the sender should close close(ch) // Closed channel returns zero value immediately v, ok := <-ch // ok == false means channel is closed and drained // Panic conditions: // - closing a nil channel // - closing an already-closed channel // - sending on a closed channel ``` ### Range over Channel ```go // Range exits when channel is closed and drained for v := range ch { process(v) } // Equivalent explicit loop for { v, ok := <-ch if !ok { break } process(v) } ``` ### Nil Channel Behavior ```go var ch chan int // nil channel // Sending or receiving on nil blocks forever // <-ch // blocks // ch <- 1 // blocks // Useful in select to disable a case dynamically func merge(a, b <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for a != nil || b != nil { select { case v, ok := <-a: if !ok { a = nil // disable this case continue } out <- v case v, ok := <-b: if !ok { b = nil // disable this case continue } out <- v } } }() return out } ``` --- ## Select ### Multi-channel Operations ```go select { case msg := <-ch1: handle(msg) case ch2 <- value: // sent successfully case <-done: return } ``` ### Timeout Pattern ```go func fetchWithTimeout(url string, timeout time.Duration) (*Response, error) { result := make(chan *Response, 1) go func() { result <- fetch(url) }() select { case resp := <-result: return resp, nil case <-time.After(timeout): return nil, fmt.Errorf("fetch %s: timed out after %v", url, timeout) } } ``` ### Non-blocking with Default ```go // Try to send/receive; skip if not ready select { case ch <- value: // sent default: // channel full or no receiver; drop or handle } // Non-blocking receive select { case v := <-ch: use(v) default: // nothing available right now } ``` ### Priority Pattern Go's select is random when multiple cases are ready. Force priority explicitly. ```go // Drain high-priority channel before processing low-priority func prioritySelect(hi, lo <-chan Job) { for { select { case job := <-hi: process(job) default: // hi empty; check both select { case job := <-hi: process(job) case job := <-lo: process(job) } } } } ``` --- ## Context ### Create Root Contexts ```go ctx := context.Background() // top-level; never cancelled ctx := context.TODO() // placeholder; replace before shipping ``` ### WithCancel ```go ctx, cancel := context.WithCancel(parent) defer cancel() // always defer to free resources go func() { <-ctx.Done() fmt.Println("cancelled:", ctx.Err()) // context.Canceled }() cancel() // trigger cancellation ``` ### WithTimeout and WithDeadline ```go ctx, cancel := context.WithTimeout(parent, 5*time.Second) defer cancel() // WithDeadline takes an absolute time deadline := time.Now().Add(5 * time.Second) ctx, cancel = context.WithDeadline(parent, deadline) defer cancel() // Check remaining time if d, ok := ctx.Deadline(); ok { remaining := time.Until(d) fmt.Println("remaining:", remaining) } ``` ### WithValue ```go type contextKey string // unexported to avoid collisions const requestIDKey contextKey = "request-id" func withRequestID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, requestIDKey, id) } func requestIDFromContext(ctx context.Context) (string, bool) { id, ok := ctx.Value(requestIDKey).(string) return id, ok } ``` ### Propagation Rules - Always pass `ctx` as the first argument to functions that do I/O - Never store context in a struct field (pass explicitly) - Derive child contexts; never modify the parent - Cancel is inherited: cancelling parent cancels all children ### HTTP Middleware Pattern ```go 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 = generateID() } ctx := withRequestID(r.Context(), id) next.ServeHTTP(w, r.WithContext(ctx)) }) } func handler(w http.ResponseWriter, r *http.Request) { id, _ := requestIDFromContext(r.Context()) // id flows through without explicit passing } ``` --- ## Sync Primitives ### Mutex ```go type SafeMap struct { mu sync.Mutex m map[string]int } func (s *SafeMap) Set(key string, val int) { s.mu.Lock() defer s.mu.Unlock() s.m[key] = val } func (s *SafeMap) Get(key string) (int, bool) { s.mu.Lock() defer s.mu.Unlock() v, ok := s.m[key] return v, ok } ``` ### RWMutex Use when reads vastly outnumber writes. ```go type Cache struct { mu sync.RWMutex data map[string]string } func (c *Cache) Get(key string) (string, bool) { c.mu.RLock() // multiple readers allowed defer c.mu.RUnlock() v, ok := c.data[key] return v, ok } func (c *Cache) Set(key, val string) { c.mu.Lock() // exclusive write defer c.mu.Unlock() c.data[key] = val } ``` ### Once ```go var ( instance *DB once sync.Once ) func GetDB() *DB { once.Do(func() { instance = connectDB() }) return instance } ``` ### WaitGroup ```go var wg sync.WaitGroup for _, url := range urls { wg.Add(1) go func(u string) { defer wg.Done() fetch(u) }(url) } wg.Wait() // block until all goroutines call Done ``` ### Pool ```go var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } func processRequest(data []byte) []byte { buf := bufPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufPool.Put(buf) }() buf.Write(data) // transform buf... return buf.Bytes() } ``` ### Atomic Operations ```go import "sync/atomic" var counter atomic.Int64 counter.Add(1) counter.Store(0) v := counter.Load() swapped := counter.CompareAndSwap(old, new) // Prefer atomic for simple counters; prefer Mutex for compound operations ``` ### When to Use Each | Primitive | Use When | |-----------|----------| | `Mutex` | Protecting a struct with multiple fields | | `RWMutex` | Read-heavy access; reads >> writes | | `Once` | One-time initialization | | `WaitGroup` | Waiting for a collection of goroutines | | `Pool` | Reusing temporary objects to reduce GC pressure | | `atomic` | Single integer/pointer with no compound operations | | Channel | Transferring ownership of data between goroutines | --- ## errgroup ### Basic Usage ```go import "golang.org/x/sync/errgroup" func fetchAll(ctx context.Context, urls []string) ([][]byte, error) { g, ctx := errgroup.WithContext(ctx) results := make([][]byte, len(urls)) for i, url := range urls { i, url := i, url g.Go(func() error { body, err := get(ctx, url) if err != nil { return fmt.Errorf("fetch %s: %w", url, err) } results[i] = body return nil }) } if err := g.Wait(); err != nil { return nil, err } return results, nil } ``` ### Limit Concurrency with SetLimit ```go g, ctx := errgroup.WithContext(ctx) g.SetLimit(10) // at most 10 goroutines at a time for _, url := range urls { url := url g.Go(func() error { return process(ctx, url) }) } return g.Wait() ``` ### Collect Results Safely Pre-allocate the result slice before launching goroutines. Each goroutine writes to its own index — no mutex needed because slice indices do not overlap. ```go type Result struct { URL string Data []byte } func gather(ctx context.Context, urls []string) ([]Result, error) { g, ctx := errgroup.WithContext(ctx) results := make([]Result, len(urls)) for i, url := range urls { i, url := i, url g.Go(func() error { data, err := get(ctx, url) if err != nil { return err } results[i] = Result{URL: url, Data: data} return nil }) } if err := g.Wait(); err != nil { return nil, err } return results, nil } ``` --- ## Worker Pool Pattern ```go type Job struct { ID int Data []byte } type Result struct { JobID int Output []byte Err error } func workerPool( ctx context.Context, jobs <-chan Job, numWorkers int, ) <-chan Result { results := make(chan Result, numWorkers) var wg sync.WaitGroup for i := 0; i < numWorkers; i++ { wg.Add(1) go func() { defer wg.Done() for { select { case <-ctx.Done(): return case job, ok := <-jobs: if !ok { return } out, err := processJob(job) results <- Result{JobID: job.ID, Output: out, Err: err} } } }() } // Close results when all workers finish go func() { wg.Wait() close(results) }() return results } func run(ctx context.Context, allJobs []Job) error { jobs := make(chan Job, len(allJobs)) for _, j := range allJobs { jobs <- j } close(jobs) results := workerPool(ctx, jobs, 5) for r := range results { if r.Err != nil { return fmt.Errorf("job %d: %w", r.JobID, r.Err) } fmt.Printf("job %d done\n", r.JobID) } return nil } ``` --- ## Fan-out / Fan-in ### Fan-out: Distribute One Channel to Many Workers ```go func fanOut(in <-chan int, n int) []<-chan int { outs := make([]<-chan int, n) for i := 0; i < n; i++ { ch := make(chan int) outs[i] = ch go func() { defer close(ch) for v := range in { ch <- v } }() } return outs } ``` ### Fan-in: Merge Multiple Channels into One ```go func fanIn(ctx context.Context, ins ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup forward := func(ch <-chan int) { defer wg.Done() for { select { case v, ok := <-ch: if !ok { return } select { case out <- v: case <-ctx.Done(): return } case <-ctx.Done(): return } } } wg.Add(len(ins)) for _, ch := range ins { go forward(ch) } go func() { wg.Wait() close(out) }() return out } ``` --- ## Pipeline Pattern Each stage reads from upstream and writes to downstream. Cancellation propagates via context. ```go func generate(ctx context.Context, nums ...int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { select { case out <- n: case <-ctx.Done(): return } } }() return out } func square(ctx context.Context, in <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for v := range in { select { case out <- v * v: case <-ctx.Done(): return } } }() return out } func filter(ctx context.Context, in <-chan int, pred func(int) bool) <-chan int { out := make(chan int) go func() { defer close(out) for v := range in { if pred(v) { select { case out <- v: case <-ctx.Done(): return } } } }() return out } func runPipeline(ctx context.Context) { nums := generate(ctx, 1, 2, 3, 4, 5) squares := square(ctx, nums) evens := filter(ctx, squares, func(n int) bool { return n%2 == 0 }) for v := range evens { fmt.Println(v) // 4, 16 } } ``` --- ## Rate Limiting ### Semaphore Pattern ```go type Semaphore chan struct{} func NewSemaphore(n int) Semaphore { return make(Semaphore, n) } func (s Semaphore) Acquire() { s <- struct{}{} } func (s Semaphore) Release() { <-s } func fetchConcurrently(ctx context.Context, urls []string, limit int) { sem := NewSemaphore(limit) var wg sync.WaitGroup for _, url := range urls { url := url wg.Add(1) go func() { defer wg.Done() sem.Acquire() defer sem.Release() fetch(ctx, url) }() } wg.Wait() } ``` ### time.Ticker Rate Limiter ```go func rateLimitedFetch(ctx context.Context, urls []string, rps int) error { ticker := time.NewTicker(time.Second / time.Duration(rps)) defer ticker.Stop() for _, url := range urls { select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: if err := fetch(ctx, url); err != nil { return err } } } return nil } ``` ### Token Bucket (using time/rate) ```go import "golang.org/x/time/rate" limiter := rate.NewLimiter(rate.Limit(100), 10) // 100 req/s, burst 10 func callAPI(ctx context.Context, req Request) error { if err := limiter.Wait(ctx); err != nil { return fmt.Errorf("rate limiter: %w", err) } return sendRequest(req) } ``` --- ## Common Mistakes ### Goroutine Leak: Blocking Send with No Receiver ```go // BAD func search(query string) <-chan Result { ch := make(chan Result) // unbuffered go func() { ch <- doSearch(query) // blocks if caller gives up }() return ch } // GOOD: buffer of 1 so goroutine never blocks func search(ctx context.Context, query string) <-chan Result { ch := make(chan Result, 1) go func() { select { case ch <- doSearch(ctx, query): case <-ctx.Done(): } }() return ch } ``` ### Race Condition: Shared Variable without Protection ```go // BAD: data race on count var count int var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() count++ // not safe }() } // GOOD: use atomic or mutex var count atomic.Int64 for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() count.Add(1) }() } ``` ### Deadlock: All Goroutines Waiting on Each Other ```go // BAD: both goroutines block trying to send before anyone reads ch := make(chan int) ch <- 1 // blocks main goroutine go func() { ch <- 2 }() // never reached // GOOD: buffer or launch reader first ch := make(chan int, 2) ch <- 1 ch <- 2 ``` ### Closing a Channel from the Wrong Side ```go // BAD: receiver closes channel; sender may still write func consumer(ch chan int) { close(ch) // panics if sender writes after this } // GOOD: only the producer closes func producer(ch chan<- int) { defer close(ch) for _, v := range data { ch <- v } } ``` ### WaitGroup Counter Mismatch ```go // BAD: Add inside goroutine; may call Wait before Add for _, item := range items { go func(item Item) { wg.Add(1) // too late defer wg.Done() process(item) }(item) } wg.Wait() // GOOD: Add before launching goroutine for _, item := range items { wg.Add(1) go func(item Item) { defer wg.Done() process(item) }(item) } wg.Wait() ``` ### Detect Races at Test Time ```go // Always run tests with the race detector // go test -race ./... // go build -race ./cmd/server ``` -
error-handling.md 15.3 KB
# Go Error Handling Reference ## Table of Contents 1. [Error Basics](#error-basics) 2. [Wrap Errors with %w](#wrap-errors-with-w) 3. [errors.Is and errors.As](#errorsis-and-errorsas) 4. [Sentinel Errors](#sentinel-errors) 5. [Custom Error Types](#custom-error-types) 6. [Error Wrapping Strategy](#error-wrapping-strategy) 7. [panic and recover](#panic-and-recover) 8. [Errors in Goroutines](#errors-in-goroutines) 9. [Multi-Error](#multi-error) 10. [Test Errors](#test-errors) 11. [Anti-Patterns](#anti-patterns) --- ## Error Basics ### The error Interface ```go type error interface { Error() string } ``` Any type implementing `Error() string` satisfies the `error` interface. ### Create Simple Errors ```go import "errors" var err1 = errors.New("something went wrong") // fmt.Errorf for formatted messages (no wrapping) err2 := fmt.Errorf("user %d not found", id) // Return nil to signal success func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } // Check error result, err := divide(10, 0) if err != nil { log.Fatal(err) } ``` --- ## Wrap Errors with %w The `%w` verb creates a wrapped error that preserves the original for inspection with `errors.Is` and `errors.As`. ```go func getUser(id int64) (*User, error) { row := db.QueryRow("SELECT * FROM users WHERE id = ?", id) if err := row.Scan(&user); err != nil { return nil, fmt.Errorf("get user %d: %w", id, err) } return &user, nil } func loadProfile(id int64) (*Profile, error) { user, err := getUser(id) if err != nil { return nil, fmt.Errorf("load profile: %w", err) } // ... return profile, nil } ``` The resulting error chain looks like: ``` load profile: get user 42: sql: no rows in result set ``` ### Unwrap the Chain ```go // errors.Unwrap returns the next error in the chain wrapped := fmt.Errorf("outer: %w", inner) inner == errors.Unwrap(wrapped) // true // Walk the full chain manually for err != nil { fmt.Println(err) err = errors.Unwrap(err) } ``` --- ## errors.Is and errors.As ### errors.Is — Identity Check Use `errors.Is` to check whether a specific sentinel error appears anywhere in the chain. ```go var ErrNotFound = errors.New("not found") err := fmt.Errorf("query: %w", ErrNotFound) errors.Is(err, ErrNotFound) // true — searches the whole chain err == ErrNotFound // false — direct comparison misses the wrapping ``` ### errors.As — Type Check Use `errors.As` to extract a typed error from anywhere in the chain. ```go type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("%s: %s", e.Field, e.Message) } err := fmt.Errorf("create user: %w", &ValidationError{Field: "email", Message: "invalid"}) var valErr *ValidationError if errors.As(err, &valErr) { fmt.Println(valErr.Field) // "email" fmt.Println(valErr.Message) // "invalid" } ``` ### Custom Is Method Implement `Is` when equality should be value-based rather than pointer-based. ```go type StatusError struct { Code int } func (e *StatusError) Error() string { return fmt.Sprintf("status %d", e.Code) } func (e *StatusError) Is(target error) bool { t, ok := target.(*StatusError) if !ok { return false } return e.Code == t.Code } ErrNotFound := &StatusError{Code: 404} err := fmt.Errorf("request: %w", &StatusError{Code: 404}) errors.Is(err, ErrNotFound) // true — matched by value ``` --- ## Sentinel Errors Sentinel errors are package-level variables used as well-known error values. ```go var ( ErrNotFound = errors.New("not found") ErrUnauthorized = errors.New("unauthorized") ErrConflict = errors.New("conflict") ) func FindUser(id int64) (*User, error) { if id == 0 { return nil, ErrNotFound } // ... } // Caller checks identity user, err := FindUser(id) if errors.Is(err, ErrNotFound) { http.Error(w, "Not Found", http.StatusNotFound) return } ``` ### Stdlib Sentinel Examples ```go io.EOF // end of stream; not an error condition io.ErrUnexpectedEOF // stream ended mid-record; is an error sql.ErrNoRows // query returned zero rows os.ErrNotExist // file does not exist (use errors.Is, not ==) context.Canceled // context was cancelled context.DeadlineExceeded // context deadline passed ``` Note: `os.ErrNotExist` wraps multiple underlying errors (`syscall.ENOENT`, etc.). Always use `errors.Is(err, os.ErrNotExist)` rather than direct comparison. --- ## Custom Error Types ### Struct Error with Extra Fields ```go type NotFoundError struct { Resource string ID int64 } func (e *NotFoundError) Error() string { return fmt.Sprintf("%s with id %d not found", e.Resource, e.ID) } func GetOrder(id int64) (*Order, error) { order := findOrder(id) if order == nil { return nil, &NotFoundError{Resource: "order", ID: id} } return order, nil } // Extract and use the extra fields var notFound *NotFoundError if errors.As(err, ¬Found) { log.Printf("missing resource: %s %d", notFound.Resource, notFound.ID) } ``` ### HTTPError with Status Code ```go type HTTPError struct { Code int Message string Cause error } func (e *HTTPError) Error() string { if e.Cause != nil { return fmt.Sprintf("HTTP %d: %s: %v", e.Code, e.Message, e.Cause) } return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message) } func (e *HTTPError) Unwrap() error { return e.Cause } // Implement Unwrap to keep the chain intact ``` --- ## Error Wrapping Strategy ### Add Context at Each Layer ```go // Repository layer: wrap with operation context func (r *UserRepo) Find(id int64) (*User, error) { var u User err := r.db.Get(&u, "SELECT * FROM users WHERE id = $1", id) if err != nil { return nil, fmt.Errorf("find user %d: %w", id, err) } return &u, nil } // Service layer: wrap with business operation context func (s *UserService) GetProfile(id int64) (*Profile, error) { user, err := s.repo.Find(id) if err != nil { return nil, fmt.Errorf("get profile: %w", err) } // ... } // Handler layer: inspect and translate for the caller func (h *Handler) handleGetProfile(w http.ResponseWriter, r *http.Request) { id := parseID(r) profile, err := h.svc.GetProfile(id) if err != nil { if errors.Is(err, sql.ErrNoRows) { http.Error(w, "not found", http.StatusNotFound) return } http.Error(w, "internal error", http.StatusInternalServerError) log.Printf("get profile: %v", err) // log full chain here return } writeJSON(w, profile) } ``` ### Message Conventions - Use lowercase for error strings (Go convention) - Use `: ` to separate context from cause - Do not end with punctuation - Do not duplicate information already in the wrapped error ```go // GOOD fmt.Errorf("parse config: %w", err) fmt.Errorf("connect to database %s: %w", dsn, err) // BAD: redundant — the wrapped error already says "failed" fmt.Errorf("failed to connect: %w", err) // BAD: capitalized fmt.Errorf("Parse config: %w", err) ``` ### Log Once, at the Top ```go // BAD: logged at every layer, duplicates output func (r *Repo) Find(id int64) (*User, error) { err := query() if err != nil { log.Printf("repo error: %v", err) // logged here return nil, fmt.Errorf("find: %w", err) } } func (s *Svc) Get(id int64) (*User, error) { u, err := r.Find(id) if err != nil { log.Printf("svc error: %v", err) // logged again return nil, fmt.Errorf("get: %w", err) } } // GOOD: wrap through; log once at the edge (handler/main) ``` --- ## panic and recover ### When panic Is Legitimate - Programmer errors that cannot be corrected at runtime (nil dereference, index out of range) - Impossible conditions in initialization (`init` or package `var` blocks) - Internal consistency violations inside a package (never cross package boundaries) ```go func mustParseURL(raw string) *url.URL { u, err := url.Parse(raw) if err != nil { panic(fmt.Sprintf("invalid hardcoded URL %q: %v", raw, err)) } return u } // Use Must* pattern for hardcoded values only; never for user input var baseURL = mustParseURL("https://api.example.com") ``` ### recover in HTTP Middleware ```go func recoveryMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { // Log with stack trace buf := make([]byte, 4096) n := runtime.Stack(buf, false) log.Printf("panic recovered: %v\n%s", rec, buf[:n]) http.Error(w, "internal server error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } ``` ### Do Not recover Across Package Boundaries A library must never let a panic escape to the caller. Recover internally and return an error. ```go func (p *Parser) Parse(input []byte) (result Result, err error) { defer func() { if rec := recover(); rec != nil { err = fmt.Errorf("parse panicked: %v", rec) } }() result = p.doParse(input) return } ``` --- ## Errors in Goroutines ### Channel-Based ```go func runAsync(ctx context.Context, fn func() error) <-chan error { errCh := make(chan error, 1) // buffer of 1 prevents leak go func() { errCh <- fn() }() return errCh } errCh := runAsync(ctx, doWork) select { case err := <-errCh: if err != nil { return fmt.Errorf("async work: %w", err) } case <-ctx.Done(): return ctx.Err() } ``` ### errgroup (Preferred) ```go g, ctx := errgroup.WithContext(ctx) g.Go(func() error { return stepA(ctx) }) g.Go(func() error { return stepB(ctx) }) // Wait returns the first non-nil error; other goroutines see ctx cancelled if err := g.Wait(); err != nil { return fmt.Errorf("pipeline: %w", err) } ``` ### Error Aggregation When all errors matter (not just the first): ```go type MultiError struct { Errors []error } func (m *MultiError) Error() string { msgs := make([]string, len(m.Errors)) for i, e := range m.Errors { msgs[i] = e.Error() } return strings.Join(msgs, "; ") } func runAll(fns []func() error) error { var mu sync.Mutex var errs []error var wg sync.WaitGroup for _, fn := range fns { fn := fn wg.Add(1) go func() { defer wg.Done() if err := fn(); err != nil { mu.Lock() errs = append(errs, err) mu.Unlock() } }() } wg.Wait() if len(errs) > 0 { return &MultiError{Errors: errs} } return nil } ``` --- ## Multi-Error ### errors.Join (Go 1.20+) ```go err1 := errors.New("validation failed on field email") err2 := errors.New("validation failed on field phone") combined := errors.Join(err1, err2) fmt.Println(combined) // validation failed on field email // validation failed on field phone errors.Is(combined, err1) // true errors.Is(combined, err2) // true ``` ### Collect Validation Errors ```go func validateUser(u User) error { var errs []error if u.Name == "" { errs = append(errs, errors.New("name is required")) } if !isValidEmail(u.Email) { errs = append(errs, fmt.Errorf("email %q is invalid", u.Email)) } if u.Age < 0 { errs = append(errs, errors.New("age must not be negative")) } return errors.Join(errs...) // nil if errs is empty } ``` --- ## Test Errors ### Check Sentinel Errors with errors.Is ```go func TestFindUser_NotFound(t *testing.T) { repo := NewRepo(testDB) _, err := repo.Find(999) if !errors.Is(err, ErrNotFound) { t.Errorf("expected ErrNotFound, got %v", err) } } ``` ### Extract Typed Errors with errors.As ```go func TestValidate_InvalidEmail(t *testing.T) { err := validateUser(User{Name: "Alice", Email: "bad"}) var valErr *ValidationError if !errors.As(err, &valErr) { t.Fatalf("expected *ValidationError, got %T: %v", err, err) } if valErr.Field != "email" { t.Errorf("expected field 'email', got %q", valErr.Field) } } ``` ### Table-Driven Error Tests ```go func TestDivide(t *testing.T) { tests := []struct { name string a, b float64 wantErr bool errIs error }{ {"normal", 10, 2, false, nil}, {"divide by zero", 10, 0, true, ErrDivisionByZero}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := Divide(tt.a, tt.b) if (err != nil) != tt.wantErr { t.Fatalf("wantErr=%v, got err=%v", tt.wantErr, err) } if tt.errIs != nil && !errors.Is(err, tt.errIs) { t.Errorf("errors.Is(%v, %v) = false", err, tt.errIs) } }) } } ``` --- ## Anti-Patterns ### Stringly-Typed Error Checks ```go // BAD: fragile; breaks on message change if err.Error() == "not found" { ... } if strings.Contains(err.Error(), "timeout") { ... } // GOOD: use sentinel or typed errors if errors.Is(err, ErrNotFound) { ... } var netErr *net.OpError if errors.As(err, &netErr) && netErr.Timeout() { ... } ``` ### Swallowing Errors ```go // BAD: silent discard result, _ := doSomething() json.Unmarshal(data, &v) // ignoring error // GOOD: handle or at minimum log result, err := doSomething() if err != nil { return fmt.Errorf("doSomething: %w", err) } if err := json.Unmarshal(data, &v); err != nil { return fmt.Errorf("unmarshal response: %w", err) } ``` ### Log and Return (Double Logging) ```go // BAD: causes duplicate log lines func getUser(id int64) (*User, error) { user, err := db.Find(id) if err != nil { log.Printf("db.Find error: %v", err) // logged here return nil, fmt.Errorf("find user: %w", err) } return user, nil } // caller also logs → same error appears twice // GOOD: wrap and propagate; log once at the boundary func getUser(id int64) (*User, error) { user, err := db.Find(id) if err != nil { return nil, fmt.Errorf("find user %d: %w", id, err) } return user, nil } ``` ### Over-Wrapping with Redundant Context ```go // BAD: "failed to" is noise; wrapped error already explains what happened return fmt.Errorf("failed to get user: failed to query database: %w", err) // GOOD: each layer adds one meaningful label return fmt.Errorf("get user %d: %w", id, err) ``` ### Panic for Expected Errors ```go // BAD: panicking on user-controlled input func ParseAge(s string) int { n, err := strconv.Atoi(s) if err != nil { panic("invalid age") // crashes the program } return n } // GOOD: return the error func ParseAge(s string) (int, error) { n, err := strconv.Atoi(s) if err != nil { return 0, fmt.Errorf("parse age %q: %w", s, err) } return n, nil } ``` ### Returning Non-nil Error with Non-zero Value ```go // BAD: caller may use the value even when err != nil func compute() (Result, error) { if bad { return Result{partial: true}, errors.New("incomplete") } } // GOOD: return zero value on error so callers don't accidentally use it func compute() (Result, error) { if bad { return Result{}, errors.New("incomplete") } } ``` -
expert-insights.md 4.3 KB
# Go HTTP and Service Patterns Reference ## Table of Contents 1. [HTTP Server (Go 1.22+ Routing)](#1-http-server-go-122-routing) 2. [Graceful Shutdown](#2-graceful-shutdown) 3. [HTTP Client Configuration](#3-http-client-configuration) 4. [JSON Request/Response Helpers](#4-json-requestresponse-helpers) --- ## 1. HTTP Server (Go 1.22+ Routing) Go 1.22 added method matching and path wildcards to `net/http.ServeMux` — no router dependency needed for most services. ```go func main() { mux := http.NewServeMux() mux.HandleFunc("GET /users/{id}", getUser) mux.HandleFunc("POST /users", createUser) server := &http.Server{ Addr: ":8080", Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 120 * time.Second, } log.Fatal(server.ListenAndServe()) } func getUser(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") // Wildcard value from the pattern user, err := userStore.GetUser(id) if err != nil { http.Error(w, "User not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } ``` Always set `ReadTimeout`, `WriteTimeout`, and `IdleTimeout` on `http.Server` — the zero values mean no timeout, which leaves the server open to slow-client resource exhaustion. ## 2. Graceful Shutdown Drain in-flight requests on SIGINT/SIGTERM instead of dropping them. ```go func main() { cfg, err := config.Load() if err != nil { log.Fatalf("loading config: %v", err) } server := &http.Server{ Addr: cfg.Addr, Handler: handler.New(cfg), } go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Printf("shutdown error: %v", err) } }() log.Printf("starting server on %s", cfg.Addr) if err := server.ListenAndServe(); err != http.ErrServerClosed { log.Fatalf("server error: %v", err) } } ``` `server.Shutdown` stops accepting new connections, then waits for active requests up to the context deadline. Compare against `http.ErrServerClosed` — a clean shutdown returns it, and treating it as fatal masks the difference between intentional and crashed exits. ## 3. HTTP Client Configuration Never use `http.DefaultClient` for production calls — it has no timeout. Build a client once and reuse it (the transport pools connections). ```go func NewHTTPClient() *http.Client { return &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, IdleConnTimeout: 90 * time.Second, }, } } func fetchJSON(ctx context.Context, url string, result any) error { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } resp, err := httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("unexpected status: %d", resp.StatusCode) } return json.NewDecoder(resp.Body).Decode(result) } ``` `MaxIdleConnsPerHost` defaults to 2 — far too low when hammering a single upstream; raise it for service-to-service traffic. ## 4. JSON Request/Response Helpers Centralize encoding/decoding so every handler behaves the same. ```go type Response struct { Data any `json:"data,omitempty"` Error string `json:"error,omitempty"` Message string `json:"message,omitempty"` } func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } func readJSON(r *http.Request, dst any) error { dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() // Reject payloads with unexpected keys if err := dec.Decode(dst); err != nil { return fmt.Errorf("decoding JSON: %w", err) } return nil } ``` `DisallowUnknownFields` turns silent typos in client payloads (`"emial"`) into explicit 400s instead of zero-valued fields. -
interfaces-generics.md 16.2 KB
# Go Interfaces and Generics Reference ## Table of Contents 1. [Interface Design Principles](#1-interface-design-principles) 2. [Interface Composition](#2-interface-composition) 3. [Type Assertions](#3-type-assertions) 4. [Empty Interface and any](#4-empty-interface-and-any) 5. [Generics Basics](#5-generics-basics) 6. [Generic Functions](#6-generic-functions) 7. [Generic Types](#7-generic-types) 8. [Constraints](#8-constraints) 9. [When NOT to Use Generics](#9-when-not-to-use-generics) 10. [Functional Options](#10-functional-options) 11. [Builder Pattern](#11-builder-pattern) 12. [Strategy via Interfaces](#12-strategy-via-interfaces) --- ## 1. Interface Design Principles **Accept interfaces, return concrete types.** Callers decide what abstraction they need; implementations should not hide their type behind an interface at the return site. ```go // BAD: returns interface, hides the concrete type unnecessarily func NewStore() Store { return &postgresStore{} } // GOOD: returns concrete pointer; callers that need the interface accept it func NewStore() *PostgresStore { return &postgresStore{} } ``` **Keep interfaces small.** One or two methods is the ideal. Large interfaces are hard to mock and hard to satisfy. ```go // BAD: one interface does too much type UserService interface { GetUser(id int64) (*User, error) CreateUser(u *User) error DeleteUser(id int64) error SendWelcomeEmail(u *User) error AuditLog(action string) error } // GOOD: split by role type UserReader interface { GetUser(id int64) (*User, error) } type UserWriter interface { CreateUser(u *User) error DeleteUser(id int64) error } type Notifier interface { SendWelcomeEmail(u *User) error } ``` **Define interfaces at the point of use (consumer), not the provider.** This avoids import cycles and keeps packages decoupled. Standard library examples of well-sized interfaces: ```go // io package: one method each type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type Closer interface { Close() error } // fmt package: one method type Stringer interface { String() string } // sort package: three methods (minimum needed for the algorithm) type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) } ``` --- ## 2. Interface Composition Embed smaller interfaces to build larger ones. Only embed what callers genuinely need together. ```go // Compose from stdlib primitives type ReadWriter interface { io.Reader io.Writer } type ReadWriteCloser interface { io.Reader io.Writer io.Closer } // Compose from your own interfaces type Repository interface { UserReader UserWriter } // Satisfy a composed interface with one struct type postgresStore struct{ db *sql.DB } func (s *postgresStore) GetUser(id int64) (*User, error) { /* ... */ } func (s *postgresStore) CreateUser(u *User) error { /* ... */ } func (s *postgresStore) DeleteUser(id int64) error { /* ... */ } var _ Repository = (*postgresStore)(nil) // compile-time check ``` The blank identifier assignment `var _ Repository = (*postgresStore)(nil)` is a zero-cost compile-time assertion that `*postgresStore` satisfies `Repository`. --- ## 3. Type Assertions ### Single-Value Form (Panics on Failure) Use only when you are certain of the type, such as immediately after a type switch. ```go var v interface{} = "hello" s := v.(string) // panics if v is not a string ``` ### Comma-OK Form (Safe) ```go var v interface{} = "hello" s, ok := v.(string) if !ok { // handle wrong type } ``` ### Type Switch The idiomatic way to branch on dynamic type. The variable `x` is narrowed to the concrete type in each case. ```go func describe(v interface{}) string { switch x := v.(type) { case string: return fmt.Sprintf("string of length %d", len(x)) case int: return fmt.Sprintf("int: %d", x) case []byte: return fmt.Sprintf("bytes: %x", x) case fmt.Stringer: return fmt.Sprintf("stringer: %s", x.String()) case nil: return "nil" default: return fmt.Sprintf("unknown type: %T", x) } } ``` --- ## 4. Empty Interface and any `any` is an alias for `interface{}` introduced in Go 1.18. Prefer `any` in new code. Use `any` only when the type is genuinely unknown at compile time: codec targets, generic containers before generics were available, or variadic logging arguments. ```go // Legitimate: JSON decode target unknown at call site func Decode(r io.Reader, dst any) error { return json.NewDecoder(r).Decode(dst) } // Legitimate: structured logging with arbitrary fields func Info(msg string, fields ...any) { /* ... */ } // Avoid: using any when a concrete type or interface would work func Process(v any) { // BAD if callers always pass *User u := v.(*User) // forced assertion everywhere } func Process(u *User) { /* ... */ } // GOOD ``` Do not use `any` as a way to avoid thinking about types. Every `any` is a deferred type error waiting for runtime. --- ## 5. Generics Basics Go generics use type parameters in square brackets. Introduced in Go 1.18. ```go // Type parameter T with constraint comparable func Contains[T comparable](slice []T, item T) bool { for _, v := range slice { if v == item { return true } } return false } // Usage - type inferred from arguments found := Contains([]string{"a", "b", "c"}, "b") // true found = Contains([]int{1, 2, 3}, 4) // false // Multiple type parameters func Map[K comparable, V any](m map[K]V, f func(V) V) map[K]V { out := make(map[K]V, len(m)) for k, v := range m { out[k] = f(v) } return out } ``` Type inference works in most cases. Provide explicit type arguments only when the compiler cannot infer them. ```go // Explicit type argument needed when return type differs from arguments func Zero[T any]() T { var zero T return zero } z := Zero[int]() // must be explicit: no argument to infer from ``` --- ## 6. Generic Functions ### Filter, Map, Reduce ```go func Filter[T any](slice []T, predicate func(T) bool) []T { var result []T for _, v := range slice { if predicate(v) { result = append(result, v) } } return result } func Map[T, U any](slice []T, f func(T) U) []U { result := make([]U, len(slice)) for i, v := range slice { result[i] = f(v) } return result } func Reduce[T, U any](slice []T, initial U, f func(U, T) U) U { acc := initial for _, v := range slice { acc = f(acc, v) } return acc } // Keys returns the keys of a map in unspecified order func Keys[K comparable, V any](m map[K]V) []K { keys := make([]K, 0, len(m)) for k := range m { keys = append(keys, k) } return keys } // Values returns the values of a map in unspecified order func Values[K comparable, V any](m map[K]V) []V { values := make([]V, 0, len(m)) for _, v := range m { values = append(values, v) } return values } ``` --- ## 7. Generic Types ### Stack ```go type Stack[T any] struct { items []T } func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) } func (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T return zero, false } n := len(s.items) - 1 item := s.items[n] s.items = s.items[:n] return item, true } func (s *Stack[T]) Len() int { return len(s.items) } ``` ### Result Type Encode success or failure without error returns scattered through call sites. ```go type Result[T any] struct { value T err error } func Ok[T any](value T) Result[T] { return Result[T]{value: value} } func Err[T any](err error) Result[T] { return Result[T]{err: err} } func (r Result[T]) Unwrap() (T, error) { return r.value, r.err } func (r Result[T]) Must() T { if r.err != nil { panic(r.err) } return r.value } ``` ### Generic Cache with TTL ```go type entry[V any] struct { value V expiresAt time.Time } type Cache[K comparable, V any] struct { mu sync.RWMutex data map[K]entry[V] ttl time.Duration } func NewCache[K comparable, V any](ttl time.Duration) *Cache[K, V] { return &Cache[K, V]{data: make(map[K]entry[V]), ttl: ttl} } func (c *Cache[K, V]) Set(key K, value V) { c.mu.Lock() defer c.mu.Unlock() c.data[key] = entry[V]{value: value, expiresAt: time.Now().Add(c.ttl)} } func (c *Cache[K, V]) Get(key K) (V, bool) { c.mu.RLock() defer c.mu.RUnlock() e, ok := c.data[key] if !ok || time.Now().After(e.expiresAt) { var zero V return zero, false } return e.value, true } ``` --- ## 8. Constraints ### Built-In Constraints ```go // comparable: supports == and != (maps, channels, basic types, structs of comparable fields) func Index[T comparable](slice []T, item T) int { for i, v := range slice { if v == item { return i } } return -1 } // any: no constraint, widest possible func Ptr[T any](v T) *T { return &v } ``` ### golang.org/x/exp/constraints ```go import "golang.org/x/exp/constraints" // Ordered: all types that support <, <=, >, >= func Min[T constraints.Ordered](a, b T) T { if a < b { return a } return b } func Max[T constraints.Ordered](a, b T) T { if a > b { return a } return b } func Clamp[T constraints.Ordered](v, lo, hi T) T { return Min(Max(v, lo), hi) } ``` ### Custom Constraints ```go // Union of specific types type Integer interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } type Float interface { ~float32 | ~float64 } type Number interface { Integer | Float } func Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n } return total } ``` ### Tilde (~) for Underlying Types `~T` includes all types whose underlying type is `T`. Without `~`, named types are excluded. ```go type Celsius float64 type Fahrenheit float64 // Without ~: Celsius and Fahrenheit do not satisfy Float type Float interface { float32 | float64 } // With ~: Celsius and Fahrenheit satisfy ~float64 type Float interface { ~float32 | ~float64 } func Convert[T ~float64](v T) T { return v * 9 / 5 + 32 } c := Celsius(100) f := Convert(c) // works because ~float64 includes Celsius ``` --- ## 9. When NOT to Use Generics **Use an interface when behavior varies by type.** Generics parametrize over structure, not behavior. If the algorithm calls different methods depending on the type, use an interface. ```go // BAD: generics cannot help here - behavior is type-specific func Process[T any](v T) { // Cannot call v.Serialize() without a constraint defining it } // GOOD: interface captures the varying behavior type Processor interface { Process() error } func Run(p Processor) error { return p.Process() } ``` **Use a concrete type when you only have one type.** Adding a type parameter for a function that only ever handles `string` or `int` adds noise with no benefit. ```go // Unnecessary generics func ParseInt[T ~string](s T) (int64, error) { return strconv.ParseInt(string(s), 10, 64) } // Simpler and clearer func ParseInt(s string) (int64, error) { return strconv.ParseInt(s, 10, 64) } ``` **Prefer `any` + type switch for heterogeneous collections** where types are enumerable and fixed. Generics do not simplify this case. --- ## 10. Functional Options The functional options pattern gives constructors optional, named parameters with default values and forward compatibility. ```go type Server struct { host string port int timeout time.Duration maxConn int logger *slog.Logger } type Option func(*Server) error func WithHost(host string) Option { return func(s *Server) error { if host == "" { return errors.New("host cannot be empty") } s.host = host return nil } } func WithPort(port int) Option { return func(s *Server) error { if port < 1 || port > 65535 { return fmt.Errorf("invalid port: %d", port) } s.port = port return nil } } func WithTimeout(d time.Duration) Option { return func(s *Server) error { if d <= 0 { return errors.New("timeout must be positive") } s.timeout = d return nil } } func WithLogger(l *slog.Logger) Option { return func(s *Server) error { s.logger = l return nil } } func NewServer(opts ...Option) (*Server, error) { s := &Server{ // defaults host: "localhost", port: 8080, timeout: 30 * time.Second, maxConn: 100, logger: slog.Default(), } for _, opt := range opts { if err := opt(s); err != nil { return nil, fmt.Errorf("applying option: %w", err) } } return s, nil } // Usage srv, err := NewServer( WithHost("0.0.0.0"), WithPort(9090), WithTimeout(time.Minute), ) ``` --- ## 11. Builder Pattern Use when construction requires many steps and partial construction is meaningful. ```go type QueryBuilder struct { table string columns []string where []string orderBy string limit int args []any err error // carry errors through the chain } func NewQuery(table string) *QueryBuilder { if table == "" { return &QueryBuilder{err: errors.New("table name required")} } return &QueryBuilder{table: table, columns: []string{"*"}} } func (q *QueryBuilder) Select(cols ...string) *QueryBuilder { if q.err != nil { return q } q.columns = cols return q } func (q *QueryBuilder) Where(condition string, args ...any) *QueryBuilder { if q.err != nil { return q } q.where = append(q.where, condition) q.args = append(q.args, args...) return q } func (q *QueryBuilder) OrderBy(col string) *QueryBuilder { if q.err != nil { return q } q.orderBy = col return q } func (q *QueryBuilder) Limit(n int) *QueryBuilder { if q.err != nil { return q } if n < 0 { q.err = fmt.Errorf("limit must be non-negative, got %d", n) return q } q.limit = n return q } func (q *QueryBuilder) Build() (string, []any, error) { if q.err != nil { return "", nil, q.err } // assemble SQL from q.table, q.columns, q.where, q.orderBy, q.limit sql := fmt.Sprintf("SELECT %s FROM %s", strings.Join(q.columns, ", "), q.table) if len(q.where) > 0 { sql += " WHERE " + strings.Join(q.where, " AND ") } if q.orderBy != "" { sql += " ORDER BY " + q.orderBy } if q.limit > 0 { sql += fmt.Sprintf(" LIMIT %d", q.limit) } return sql, q.args, nil } // Usage sql, args, err := NewQuery("users"). Select("id", "name", "email"). Where("active = $1", true). Where("role = $2", "admin"). OrderBy("name"). Limit(25). Build() ``` --- ## 12. Strategy via Interfaces Swap algorithms at runtime by accepting an interface. The caller chooses the strategy; the function does not need to know the implementation. ```go // Define the strategy interface type Hasher interface { Hash(data []byte) []byte Name() string } // Multiple implementations type SHA256Hasher struct{} func (SHA256Hasher) Hash(data []byte) []byte { h := sha256.Sum256(data) return h[:] } func (SHA256Hasher) Name() string { return "sha256" } type Blake2Hasher struct{} func (Blake2Hasher) Hash(data []byte) []byte { h := blake2b.Sum256(data) return h[:] } func (Blake2Hasher) Name() string { return "blake2b" } // Consumer accepts the interface - does not care about the algorithm type FileStore struct { hasher Hasher } func NewFileStore(h Hasher) *FileStore { return &FileStore{hasher: h} } func (fs *FileStore) Store(path string, data []byte) error { checksum := fs.hasher.Hash(data) // write data and checksum to path return writeWithChecksum(path, data, checksum, fs.hasher.Name()) } // Swap strategies at call site fastStore := NewFileStore(Blake2Hasher{}) secureStore := NewFileStore(SHA256Hasher{}) ``` This pattern is the Go equivalent of the Gang of Four Strategy pattern. It composes without inheritance and is trivially testable: inject a `mockHasher` that returns fixed bytes. -
performance.md 17.3 KB
# Go Performance Reference ## Table of Contents 1. [pprof](#pprof) 2. [go tool trace](#go-tool-trace) 3. [Benchmarks](#benchmarks) 4. [Escape Analysis](#escape-analysis) 5. [Memory Optimization](#memory-optimization) 6. [String Performance](#string-performance) 7. [Struct Alignment](#struct-alignment) 8. [Map Performance](#map-performance) 9. [I/O Performance](#io-performance) 10. [Inlining](#inlining) 11. [Common Performance Anti-Patterns](#common-performance-anti-patterns) --- ## pprof ### Enable pprof in a Production Server ```go import ( "net/http" _ "net/http/pprof" // Side-effect import registers handlers on DefaultServeMux ) func main() { // Serve pprof on a separate port — never expose this publicly go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() // ... start your actual server } ``` ### Collect and Analyze CPU Profiles ```bash # 30-second CPU profile from a running server go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 # Inside pprof interactive shell (pprof) top10 # Top 10 functions by CPU (pprof) list myFunc # Annotated source for a function (pprof) web # Open flame graph in browser (requires graphviz) (pprof) png > cpu.png # Export to image ``` ### Collect and Analyze Memory Profiles ```bash # Heap profile (in-use allocations) go tool pprof http://localhost:6060/debug/pprof/heap # Allocation profile (all allocations since start) go tool pprof http://localhost:6060/debug/pprof/allocs # Inside pprof (pprof) top # Top allocators (pprof) inuse_space # Sort by in-use bytes (pprof) alloc_objects # Sort by allocation count ``` ### Profile Goroutines ```bash # Goroutine profile — shows all running goroutines with stack traces go tool pprof http://localhost:6060/debug/pprof/goroutine # Or view in browser for a quick human-readable dump curl http://localhost:6060/debug/pprof/goroutine?debug=2 ``` ### Write Profiles Programmatically ```go import "runtime/pprof" // CPU profile f, _ := os.Create("cpu.prof") pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() // Memory profile (write at end of program or specific checkpoint) f, _ := os.Create("mem.prof") runtime.GC() // Force GC for accurate snapshot pprof.WriteHeapProfile(f) f.Close() ``` ### Compare Two Profiles ```bash # Capture baseline and after a change, then diff them go tool pprof -base baseline.prof current.prof ``` --- ## go tool trace The tracer records goroutine scheduling, GC pauses, and syscalls at microsecond resolution. ### Record a Trace ```bash # From a live server curl http://localhost:6060/debug/pprof/trace?seconds=5 > trace.out # Or programmatically import "runtime/trace" f, _ := os.Create("trace.out") trace.Start(f) defer trace.Stop() ``` ### Analyze a Trace ```bash go tool trace trace.out # Opens browser-based UI ``` Key views in the UI: - **Goroutine analysis**: Which goroutines ran, for how long, what blocked them - **View trace**: Timeline of all goroutines across P (processor) threads - **Minimum mutator utilization (MMU)**: Percentage of time your code ran vs GC ### Identify Scheduling Latency Look for goroutines spending time in "Runnable" state — this means they are ready to run but waiting for a P. Signs of over-subscription: too many goroutines competing for `GOMAXPROCS` slots. ```go // Instrument specific regions in the trace import "runtime/trace" ctx, task := trace.NewTask(ctx, "processOrder") defer task.End() trace.WithRegion(ctx, "validateInput", func() { validate(input) }) ``` --- ## Benchmarks ### Write Effective Benchmarks ```go func BenchmarkProcess(b *testing.B) { data := generateLargeInput() // Setup before timer b.ResetTimer() // Exclude setup from measurement for i := 0; i < b.N; i++ { Process(data) } } ``` ### Use b.StopTimer / b.StartTimer for Per-Iteration Setup ```go func BenchmarkSort(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() data := generateUnsortedSlice(1000) // Re-create per iteration b.StartTimer() sort.Ints(data) } } ``` ### Allocations Matter — Report Them ```go func BenchmarkParse(b *testing.B) { b.ReportAllocs() // Show allocs/op and B/op in output for i := 0; i < b.N; i++ { Parse(input) } } ``` ### Run Benchmarks ```bash go test -bench=. -benchmem -count=5 ./... # Run only matching benchmarks go test -bench=BenchmarkProcess -benchmem -run=^$ ./pkg/processor # -run=^$ suppresses tests, runs only benchmarks ``` ### Compare Results with benchstat ```bash go install golang.org/x/perf/cmd/benchstat@latest # Capture two runs go test -bench=. -count=10 ./... > before.txt # Make your change go test -bench=. -count=10 ./... > after.txt benchstat before.txt after.txt ``` Output shows statistical significance: `p < 0.05` means the difference is likely real, not noise. Use `-count=10` or more for reliable statistics. --- ## Escape Analysis ### Inspect Escape Decisions ```bash go build -gcflags='-m' ./... # Basic escape analysis go build -gcflags='-m=2' ./... # Verbose (shows escape reason) go test -gcflags='-m' ./... # On test files ``` ### Understand Heap vs Stack Values escape to the heap when: - Their address is returned or stored in a longer-lived structure - They are assigned to an interface - They are too large for the stack (default stack starts at 8KB, goroutines grow as needed but large locals still escape) - The compiler cannot prove the lifetime is bounded ```go // Stack allocated — does NOT escape func sumSquares(nums []int) int { total := 0 // total stays on stack for _, n := range nums { total += n * n } return total } // Heap allocated — escapes because address is returned func newCounter() *int { n := 0 return &n // n escapes: "moved to heap: n" } // Interface assignment causes escape func logValue(v interface{}) { // Passing int here allocates on heap fmt.Println(v) } ``` ### Reduce Allocations with Value Receivers ```go // BAD: Pointer causes allocation when assigned to interface type Point struct{ X, Y float64 } func (p *Point) String() string { return fmt.Sprintf("(%f, %f)", p.X, p.Y) } // GOOD: Value receiver, may stay on stack func (p Point) String() string { return fmt.Sprintf("(%f, %f)", p.X, p.Y) } ``` --- ## Memory Optimization ### Use sync.Pool for Frequently Allocated Short-Lived Objects ```go var bufPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) }, } func formatMessage(data []byte) string { buf := bufPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufPool.Put(buf) }() buf.Write(data) // ... format into buf return buf.String() } ``` Pool objects may be collected by GC at any time. Never store state that must survive across GC cycles in a pool. ### Pre-Allocate Slices ```go // BAD: O(n) reallocations as slice grows var results []Result for _, item := range items { results = append(results, process(item)) } // GOOD: Single allocation results := make([]Result, 0, len(items)) for _, item := range items { results = append(results, process(item)) } ``` ### Reuse Slices Across Calls ```go type Processor struct { buf []byte // Reused across calls } func (p *Processor) Process(input []byte) []byte { p.buf = p.buf[:0] // Reset length, keep capacity p.buf = append(p.buf, input...) // ... transform p.buf return p.buf } ``` ### Avoid Large Value Copies ```go type LargeStruct struct { Data [4096]byte // ... } // BAD: Copies 4KB on every call func processLarge(s LargeStruct) { ... } // GOOD: Pass pointer func processLarge(s *LargeStruct) { ... } ``` --- ## String Performance ### Use strings.Builder for Concatenation ```go // BAD: Creates a new string on every iteration var result string for _, s := range parts { result += s + ", " } // GOOD: Single allocation var sb strings.Builder sb.Grow(estimatedSize) // Pre-grow if you know the size for _, s := range parts { sb.WriteString(s) sb.WriteString(", ") } result := sb.String() ``` ### Convert Between []byte and string Without Allocation The standard `string(b)` and `[]byte(s)` conversions always allocate. For read-only access within a single goroutine, use `unsafe`: ```go import "unsafe" // []byte to string — zero copy, safe only if you don't modify b afterward func bytesToString(b []byte) string { return unsafe.String(unsafe.SliceData(b), len(b)) } // string to []byte — zero copy, safe only for reads func stringToBytes(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s)) } ``` These are valid as of Go 1.20. Do not use the older `*(*string)(unsafe.Pointer(&b))` pattern. ### Avoid fmt.Sprintf for Simple Concatenation ```go // BAD: Heap allocation, format parsing overhead key := fmt.Sprintf("%s:%d", prefix, id) // GOOD: strconv is faster for basic conversions key := prefix + ":" + strconv.Itoa(id) // GOOD: For multiple parts, strings.Join or Builder key := strings.Join([]string{prefix, strconv.Itoa(id)}, ":") ``` --- ## Struct Alignment The CPU reads memory in aligned chunks. Padding bytes are inserted to satisfy alignment requirements. Reordering fields from largest to smallest eliminates wasted bytes. ```go // BAD: 24 bytes due to padding type BadLayout struct { Active bool // 1 byte + 7 bytes padding Count int64 // 8 bytes Flag bool // 1 byte + 7 bytes padding } // GOOD: 16 bytes, no padding type GoodLayout struct { Count int64 // 8 bytes Active bool // 1 byte Flag bool // 1 byte + 6 bytes padding (to align to 8) } ``` ### Check Sizes and Padding ```go import "unsafe" fmt.Println(unsafe.Sizeof(BadLayout{})) // 24 fmt.Println(unsafe.Sizeof(GoodLayout{})) // 16 ``` ### Use fieldalignment to Find Problems Automatically ```bash go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest fieldalignment ./... # Report structs with inefficient layout fieldalignment -fix ./... # Rewrite fields automatically ``` ### Cache Line Considerations for Concurrent Structs Fields accessed by different goroutines should be on separate cache lines (64 bytes) to prevent false sharing: ```go type Counters struct { reads int64 _ [56]byte // Pad to fill cache line writes int64 } ``` --- ## Map Performance ### Pre-Size Maps ```go // BAD: Map grows incrementally, triggering multiple rehashes m := make(map[string]int) for _, item := range items { m[item.Key] = item.Value } // GOOD: Single allocation m := make(map[string]int, len(items)) for _, item := range items { m[item.Key] = item.Value } ``` ### Use Switch for Small Key Sets For fewer than ~8 fixed string keys, a switch statement is faster than a map due to branch prediction and no hashing overhead: ```go // Faster for small, known sets func httpMethodCode(method string) int { switch method { case "GET": return 0 case "POST": return 1 case "PUT": return 2 case "DELETE": return 3 default: return -1 } } ``` ### Choose sync.Map vs Sharded Map `sync.Map` is optimized for two specific cases: 1. Write-once, read-many (mostly reads after initial population) 2. Many goroutines reading/writing disjoint keys For general concurrent access with frequent writes, a sharded map with per-shard mutexes outperforms `sync.Map`: ```go const numShards = 256 type ShardedMap struct { shards [numShards]struct { sync.RWMutex m map[string]interface{} } } func (sm *ShardedMap) shard(key string) int { h := fnv.New32() h.Write([]byte(key)) return int(h.Sum32()) % numShards } func (sm *ShardedMap) Get(key string) (interface{}, bool) { s := &sm.shards[sm.shard(key)] s.RLock() v, ok := s.m[key] s.RUnlock() return v, ok } ``` --- ## I/O Performance ### Always Wrap with bufio Unbuffered reads and writes issue a syscall for every call. Buffering batches them: ```go // BAD: Syscall per line f, _ := os.Open("data.txt") scanner := bufio.NewScanner(f) // This is already buffered — correct // BAD: Syscall per Write call f, _ := os.Create("out.txt") fmt.Fprintln(f, line) // Goes through direct write // GOOD: Buffered writes f, _ := os.Create("out.txt") bw := bufio.NewWriterSize(f, 64*1024) // 64KB buffer defer bw.Flush() fmt.Fprintln(bw, line) ``` ### Use io.Copy for Efficient Transfers `io.Copy` uses a 32KB internal buffer and delegates to `sendfile(2)` or `splice(2)` when both sides support it (e.g., `*os.File` to `*net.TCPConn`): ```go // Efficient file download with no intermediate allocation func serveFile(w http.ResponseWriter, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() _, err = io.Copy(w, f) return err } ``` ### Use io.Pipe for Producer-Consumer Pipelines ```go pr, pw := io.Pipe() go func() { defer pw.Close() json.NewEncoder(pw).Encode(largeStruct) // Streams without buffering whole JSON }() http.Post(url, "application/json", pr) ``` ### Limit Reads to Avoid Memory Exhaustion ```go const maxBodySize = 1 << 20 // 1MB r.Body = http.MaxBytesReader(w, r.Body, maxBodySize) if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "request too large or invalid", http.StatusBadRequest) return } ``` --- ## Inlining The compiler inlines small functions to eliminate call overhead. A function is inlined when its "cost" (an internal AST node count) stays below a threshold (~80 nodes). ### Check What Gets Inlined ```bash go build -gcflags='-m' ./... # Output includes: # ./pkg/math.go:12:6: can inline Add # ./pkg/handler.go:45:12: inlining call to Add # ./pkg/handler.go:60:5: cannot inline processLarge: function too complex ``` ### Write Inlineable Functions ```go // Inlineable: small, no closures, no defer func clamp(v, min, max int) int { if v < min { return min } if v > max { return max } return v } // NOT inlineable: contains a closure func makeAdder(n int) func(int) int { return func(x int) int { return x + n } } ``` ### Prevent Inlining ```go //go:noinline // Force a function to never be inlined (useful for benchmarking) func expensiveOperation(data []byte) Result { // ... } ``` Use `//go:noinline` in benchmarks when you want to measure the cost of a function call itself, or to prevent the compiler from optimizing away a call you want to measure. --- ## Common Performance Anti-Patterns ### Reflection in Hot Paths Reflection bypasses type-system optimizations, performs map lookups, and allocates. Avoid in code called frequently: ```go // BAD: reflect.ValueOf allocates, method lookup is slow func setField(obj interface{}, name string, value interface{}) { v := reflect.ValueOf(obj).Elem() v.FieldByName(name).Set(reflect.ValueOf(value)) } // GOOD: Generated code or type switch func applyUpdate(u *User, field string, value interface{}) { switch field { case "Name": u.Name = value.(string) case "Email": u.Email = value.(string) } } ``` ### fmt.Sprintf in Hot Paths `fmt.Sprintf` parses a format string, uses reflection, and typically allocates: ```go // BAD in hot path key := fmt.Sprintf("user:%d:session:%s", userID, sessionID) // GOOD: strconv + concatenation key := "user:" + strconv.FormatInt(userID, 10) + ":session:" + sessionID // GOOD for complex formatting: pre-build a template or use strings.Builder ``` ### Unnecessary Allocations in Loops ```go // BAD: Allocates a new map every iteration for _, item := range items { m := map[string]int{"count": item.Count} process(m) } // GOOD: Allocate once, reuse m := make(map[string]int, 1) for _, item := range items { m["count"] = item.Count process(m) // Clear before next iteration if needed for k := range m { delete(m, k) } } ``` ### Goroutine Leak from Unclosed Channels ```go // BAD: Goroutine blocked forever if consumer exits early func generate(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n // Blocks forever if nobody reads } close(out) }() return out } // GOOD: Use context for cancellation func generate(ctx context.Context, nums ...int) <-chan int { out := make(chan int, len(nums)) go func() { defer close(out) for _, n := range nums { select { case out <- n: case <-ctx.Done(): return } } }() return out } ``` ### Copying a Mutex Mutexes must not be copied after first use. Copying a locked mutex will deadlock; copying an unlocked mutex silently creates a new, independent lock: ```go // BAD: Copies the mutex type Cache struct{ mu sync.Mutex; data map[string]int } func copyCache(c Cache) Cache { return c } // Copies mu — wrong // GOOD: Always pass and return pointers for types containing mutexes func processCache(c *Cache) { ... } ``` ### Defer in a Tight Loop Defers execute at function return, not loop iteration. Inside a loop, defers pile up and all run together at the end: ```go // BAD: All files stay open until the function returns for _, path := range paths { f, _ := os.Open(path) defer f.Close() // Runs at function exit, not loop end process(f) } // GOOD: Wrap in a closure or extract to a helper for _, path := range paths { func() { f, _ := os.Open(path) defer f.Close() // Now runs at end of this closure process(f) }() } ``` -
project-structure.md 12.6 KB
# Go Project Structure Reference ## Table of Contents 1. [Standard Project Layout](#standard-project-layout) 2. [Module Management](#module-management) 3. [Workspace Mode](#workspace-mode) 4. [Build Tags](#build-tags) 5. [Build Configuration](#build-configuration) 6. [Makefile and Justfile Patterns](#makefile-and-justfile-patterns) 7. [Linting](#linting) 8. [Code Generation](#code-generation) 9. [Release](#release) --- ## Standard Project Layout The Go community has converged on a layout that separates public, private, and executable code clearly. ``` myapp/ ├── cmd/ # Executable entry points (one dir per binary) │ ├── server/ │ │ └── main.go │ └── worker/ │ └── main.go ├── internal/ # Private packages (import-restricted by go toolchain) │ ├── config/ │ │ └── config.go │ ├── handler/ │ │ └── user.go │ ├── service/ │ │ └── user.go │ └── repository/ │ └── user.go ├── pkg/ # Public packages (importable by external projects) │ └── validator/ │ └── validator.go ├── api/ # API definitions (OpenAPI, protobuf, gRPC) │ └── openapi.yaml ├── web/ # Web assets, templates ├── scripts/ # Build, install, CI scripts ├── configs/ # Config file templates ├── testdata/ # Test fixtures (go tools ignore dirs starting with "testdata") ├── go.mod ├── go.sum ├── Makefile (or justfile) └── README.md ``` ### When to Use Each Directory **cmd/**: Place `main.go` files here. Each subdirectory is a separate binary. Keep main.go thin — parse flags, load config, wire dependencies, then call into `internal/`. **internal/**: Use for everything application-specific. The Go toolchain enforces that packages under `internal/` can only be imported by code in the parent directory tree. Use this for business logic, handlers, database access. **pkg/**: Only create this if you genuinely want external projects to import your code. Most applications do not need `pkg/` at all. Avoid the anti-pattern of putting everything in `pkg/` just to follow the template. ```go // cmd/server/main.go — wire dependencies here, logic lives in internal/ func main() { cfg, err := config.Load() if err != nil { log.Fatalf("loading config: %v", err) } db, err := database.Connect(cfg.DatabaseURL) if err != nil { log.Fatalf("connecting to database: %v", err) } repo := repository.NewUser(db) svc := service.NewUser(repo) h := handler.NewUser(svc) srv := &http.Server{Addr: cfg.Addr, Handler: h.Routes()} log.Fatal(srv.ListenAndServe()) } ``` --- ## Module Management ### go.mod Directives ``` module github.com/myorg/myapp go 1.22 require ( github.com/lib/pq v1.10.9 golang.org/x/sync v0.6.0 ) // Replace a dependency with a local version during development replace github.com/myorg/shared => ../shared // Exclude a specific broken version exclude github.com/bad/module v1.2.3 ``` **require**: Direct and indirect dependencies. The `// indirect` comment marks transitive dependencies that aren't directly imported by your code but are required by your dependencies. **replace**: Use for local development of shared modules, or to patch a dependency without forking. Remove before merging to main — `replace` directives break downstream consumers. **exclude**: Prevents a specific version from being selected by MVS. Useful when a version has a known bug and you want to force a later version. ### Manage go.sum `go.sum` contains the expected cryptographic checksums of module content. Commit it to version control. Never edit it manually. Regenerate with: ```bash go mod tidy # Add missing, remove unused dependencies go mod verify # Verify checksums against go.sum go mod download # Pre-download modules (useful in Docker layers) ``` ### Private Modules Configure the Go toolchain to skip the public checksum database and proxy for private code: ```bash # Tell go to bypass proxy and sumdb for private modules export GOPRIVATE=github.com/myorg/* # Separate controls for proxy and sumdb export GONOSUMCHECK=github.com/myorg/* export GONOPROXY=github.com/myorg/* # Use a corporate proxy for public modules export GOPROXY=https://proxy.company.com,direct ``` In CI, set these as environment variables. For `.netrc`-based auth with private GitHub: ``` machine github.com login git password <personal-access-token> ``` --- ## Workspace Mode Workspaces allow multiple modules to be developed together without `replace` directives. ```bash go work init ./app ./shared ./tools # Creates go.work go work use ./new-module # Add another module go work sync # Sync dependencies ``` **go.work file:** ``` go 1.22 use ( ./app ./shared ./tools ) ``` ### When Workspaces Help - Developing two modules simultaneously (e.g., a library and a consuming app) - Monorepo with multiple Go modules - Testing unreleased changes to a shared package before publishing ### When Workspaces Do Not Help - Single-module repos (no benefit) - Production builds — exclude `go.work` from Docker contexts with `.dockerignore` ``` # .dockerignore go.work go.work.sum ``` --- ## Build Tags Build tags control which files are included in a build. The modern syntax uses `//go:build`. ```go //go:build integration package mypackage ``` ```go //go:build linux && amd64 package mypackage ``` ```go //go:build !windows package mypackage ``` ### Common Tag Patterns ```go //go:build ignore // Exclude from normal builds (e.g., generation scripts) //go:build integration // Integration tests requiring real external services //go:build e2e // End-to-end tests //go:build cgo // Only build when CGO is enabled ``` ### Run Builds with Tags ```bash go test -tags integration ./... go build -tags production ./cmd/server go vet -tags integration ./... ``` ### Separate Integration Tests ```go //go:build integration package repository_test import ( "testing" "os" ) func TestUserRepository_Integration(t *testing.T) { dsn := os.Getenv("TEST_DATABASE_URL") if dsn == "" { t.Skip("TEST_DATABASE_URL not set") } // ... test against real database } ``` --- ## Build Configuration ### Inject Version Information at Build Time ```go // internal/version/version.go package version var ( Version = "dev" GitCommit = "unknown" BuildDate = "unknown" ) ``` ```bash go build \ -ldflags="-X github.com/myorg/myapp/internal/version.Version=1.2.3 \ -X github.com/myorg/myapp/internal/version.GitCommit=$(git rev-parse --short HEAD) \ -X github.com/myorg/myapp/internal/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ ./cmd/server ``` ### Build Static Binaries ```bash CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ -ldflags="-s -w" \ -trimpath \ -o bin/server \ ./cmd/server ``` - `CGO_ENABLED=0`: Disable cgo, produce a statically linked binary - `-s -w`: Strip debug info and DWARF symbols (reduces binary size ~30%) - `-trimpath`: Remove local file paths from the binary (reproducible builds, avoids leaking local paths) ### Cross-Compile ```bash GOOS=windows GOARCH=amd64 go build ./cmd/server GOOS=darwin GOARCH=arm64 go build ./cmd/server GOOS=linux GOARCH=arm64 go build ./cmd/server ``` --- ## Makefile and Justfile Patterns ### Makefile ```makefile BINARY := bin/server VERSION := $(shell git describe --tags --always --dirty) COMMIT := $(shell git rev-parse --short HEAD) BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS := -X main.version=$(VERSION) -X main.commit=$(COMMIT) .PHONY: build test lint generate clean docker build: CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -trimpath -o $(BINARY) ./cmd/server test: go test -race -coverprofile=coverage.out ./... go tool cover -html=coverage.out -o coverage.html test-integration: go test -race -tags integration ./... lint: golangci-lint run ./... generate: go generate ./... clean: rm -rf bin/ coverage.out coverage.html docker: docker build --build-arg VERSION=$(VERSION) -t myapp:$(VERSION) . tidy: go mod tidy go mod verify ``` ### Justfile ```just version := `git describe --tags --always --dirty` commit := `git rev-parse --short HEAD` build: CGO_ENABLED=0 go build \ -ldflags="-X main.version={{version}} -X main.commit={{commit}}" \ -trimpath -o bin/server ./cmd/server test: go test -race -coverprofile=coverage.out ./... test-integration: go test -race -tags integration ./... lint: golangci-lint run ./... generate: go generate ./... tidy: go mod tidy && go mod verify ``` --- ## Linting ### Install and Run golangci-lint ```bash # Install (do not use go install — use the official installer) curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \ | sh -s -- -b $(go env GOPATH)/bin v1.57.2 golangci-lint run ./... golangci-lint run --fix ./... # Auto-fix where possible ``` ### Recommended .golangci.yml ```yaml linters: enable: - errcheck # Check all error returns are handled - gosimple # Simplification suggestions - govet # go vet checks - ineffassign # Detect unused variable assignments - staticcheck # Comprehensive static analysis - unused # Detect unused code - gofmt # Enforce gofmt formatting - goimports # Enforce import grouping - gocritic # Opinionated style checks - misspell # Catch common misspellings - prealloc # Suggest slice pre-allocation - exhaustive # Enforce exhaustive enum switches - noctx # Detect HTTP requests without context linters-settings: errcheck: check-blank: true govet: enable-all: true gocritic: enabled-tags: [diagnostic, style, performance] issues: exclude-rules: - path: _test\.go linters: [errcheck] # Relax error checking in tests ``` ### Suppress Specific Warnings ```go //nolint:errcheck // Intentionally ignoring close error on best-effort cleanup defer f.Close() //nolint:exhaustive // Default case handles unrecognized values switch status { case Active: return "active" default: return "unknown" } ``` --- ## Code Generation ### go generate Place `//go:generate` directives in the file where the generated output belongs conceptually. ```go // internal/domain/status.go //go:generate stringer -type=Status type Status int const ( Active Status = iota Inactive Pending ) ``` ```go // internal/repository/mock_store.go (or a dedicated mocks/ dir) //go:generate mockgen -source=store.go -destination=mock_store.go -package=repository ``` Run all generators: ```bash go generate ./... ``` ### Embed Static Files ```go import "embed" //go:embed templates/*.html var templateFS embed.FS //go:embed migrations var migrationsFS embed.FS //go:embed static/app.js static/app.css var staticFiles embed.FS ``` - Paths are relative to the file containing the directive - Supports glob patterns and directories - Embedded files are read-only at runtime --- ## Release ### goreleaser ```yaml # .goreleaser.yml project_name: myapp builds: - id: server main: ./cmd/server binary: server env: - CGO_ENABLED=0 goos: [linux, darwin, windows] goarch: [amd64, arm64] ldflags: - -s -w - -X main.version={{.Version}} - -X main.commit={{.Commit}} - -X main.date={{.Date}} flags: - -trimpath archives: - format: tar.gz format_overrides: - goos: windows format: zip checksum: name_template: "checksums.txt" changelog: sort: asc filters: exclude: ['^docs:', '^test:', Merge pull request] ``` ```bash # Dry run to verify configuration goreleaser release --snapshot --clean # Publish a real release (requires GITHUB_TOKEN) goreleaser release --clean ``` ### Manual Cross-Compile Script ```bash #!/usr/bin/env bash set -euo pipefail VERSION=$(git describe --tags --always) PLATFORMS=("linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64" "windows/amd64") for platform in "${PLATFORMS[@]}"; do GOOS="${platform%/*}" GOARCH="${platform#*/}" output="dist/server_${GOOS}_${GOARCH}" [[ "$GOOS" == "windows" ]] && output="${output}.exe" echo "Building $output" CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" go build \ -ldflags="-s -w -X main.version=${VERSION}" \ -trimpath -o "$output" ./cmd/server done ``` -
testing.md 16.7 KB
# Go Testing Reference ## Table of Contents 1. [Table-Driven Tests](#1-table-driven-tests) 2. [Test Helpers](#2-test-helpers) 3. [Mocking with Interfaces](#3-mocking-with-interfaces) 4. [testify](#4-testify) 5. [httptest](#5-httptest) 6. [Benchmarks](#6-benchmarks) 7. [Fuzz Testing](#7-fuzz-testing) 8. [Integration Tests](#8-integration-tests) 9. [Golden Files](#9-golden-files) 10. [Test Fixtures and TestMain](#10-test-fixtures-and-testmain) 11. [Race Detection](#11-race-detection) 12. [Coverage](#12-coverage) --- ## 1. Table-Driven Tests Write tests as a slice of structs. Name the slice `tests` and each element `tt`. Run each with `t.Run`. ```go func TestDivide(t *testing.T) { t.Parallel() tests := []struct { name string dividend float64 divisor float64 want float64 wantErr bool }{ {name: "positive", dividend: 10, divisor: 2, want: 5}, {name: "negative divisor", dividend: 10, divisor: -2, want: -5}, {name: "fractional result", dividend: 7, divisor: 2, want: 3.5}, {name: "zero divisor", dividend: 10, divisor: 0, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() // run subtests in parallel when safe got, err := Divide(tt.dividend, tt.divisor) if tt.wantErr { if err == nil { t.Fatal("expected error, got nil") } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if got != tt.want { t.Errorf("Divide(%v, %v) = %v, want %v", tt.dividend, tt.divisor, got, tt.want) } }) } } ``` Use `t.Fatal` when further execution is meaningless. Use `t.Error` to accumulate multiple failures. Capture loop variables before `t.Parallel()` in Go versions before 1.22 (Go 1.22+ fixes loop variable capture automatically). --- ## 2. Test Helpers ### t.Helper Mark helper functions with `t.Helper()` so failures report the caller's line, not the helper's. ```go func assertNoError(t *testing.T, err error) { t.Helper() if err != nil { t.Fatalf("unexpected error: %v", err) } } func assertEqual[T comparable](t *testing.T, got, want T) { t.Helper() if got != want { t.Errorf("got %v, want %v", got, want) } } ``` ### t.Cleanup Register cleanup functions that run even if the test panics or calls `t.Fatal`. ```go func newTestDB(t *testing.T) *sql.DB { t.Helper() db, err := sql.Open("sqlite3", ":memory:") if err != nil { t.Fatalf("opening db: %v", err) } t.Cleanup(func() { if err := db.Close(); err != nil { t.Errorf("closing db: %v", err) } }) return db } ``` ### t.TempDir Use `t.TempDir()` instead of `os.MkdirTemp`. It is automatically removed after the test. ```go func TestWriteFile(t *testing.T) { dir := t.TempDir() // cleaned up automatically path := filepath.Join(dir, "output.txt") err := WriteFile(path, []byte("hello")) if err != nil { t.Fatal(err) } got, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if string(got) != "hello" { t.Errorf("got %q, want %q", got, "hello") } } ``` ### testdata Directory Place static input files in `testdata/`. The Go tool ignores this directory for builds. Reference files relative to the package root using `filepath.Join("testdata", "input.json")`. ```go func TestParseConfig(t *testing.T) { data, err := os.ReadFile(filepath.Join("testdata", "config.json")) if err != nil { t.Fatal(err) } cfg, err := ParseConfig(data) if err != nil { t.Fatalf("ParseConfig: %v", err) } if cfg.Port != 8080 { t.Errorf("got port %d, want 8080", cfg.Port) } } ``` --- ## 3. Mocking with Interfaces Define narrow interfaces at the point of use, not in the package that implements them. ```go // Define the interface (in the consumer's package) type UserStore interface { GetUser(ctx context.Context, id int64) (*User, error) SaveUser(ctx context.Context, u *User) error } // Hand-rolled mock (no external dependencies) type mockUserStore struct { getUser func(ctx context.Context, id int64) (*User, error) saveUser func(ctx context.Context, u *User) error calls []string } func (m *mockUserStore) GetUser(ctx context.Context, id int64) (*User, error) { m.calls = append(m.calls, "GetUser") if m.getUser != nil { return m.getUser(ctx, id) } return nil, nil } func (m *mockUserStore) SaveUser(ctx context.Context, u *User) error { m.calls = append(m.calls, "SaveUser") if m.saveUser != nil { return m.saveUser(ctx, u) } return nil } // Test using the mock func TestUserService_Promote(t *testing.T) { store := &mockUserStore{ getUser: func(_ context.Context, id int64) (*User, error) { return &User{ID: id, Role: "member"}, nil }, saveUser: func(_ context.Context, u *User) error { if u.Role != "admin" { return fmt.Errorf("expected role admin, got %s", u.Role) } return nil }, } svc := NewUserService(store) err := svc.Promote(context.Background(), 42) if err != nil { t.Fatalf("Promote: %v", err) } if len(store.calls) != 2 { t.Errorf("expected 2 calls, got %d: %v", len(store.calls), store.calls) } } ``` --- ## 4. testify Install: `go get github.com/stretchr/testify`. ### assert vs require `assert` logs failure and continues. `require` stops the test immediately (calls `t.FailNow`). ```go import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestUserCreation(t *testing.T) { user, err := NewUser("alice@example.com") require.NoError(t, err) // stop if error require.NotNil(t, user) // stop if nil assert.Equal(t, "alice@example.com", user.Email) assert.Empty(t, user.PasswordHash) // multiple checks continue on failure assert.WithinDuration(t, time.Now(), user.CreatedAt, time.Second) } ``` ### testify/suite Group related tests with shared setup/teardown. ```go import "github.com/stretchr/testify/suite" type UserSuite struct { suite.Suite db *sql.DB svc *UserService } func (s *UserSuite) SetupSuite() { db, err := sql.Open("sqlite3", ":memory:") s.Require().NoError(err) s.db = db s.svc = NewUserService(db) } func (s *UserSuite) TearDownSuite() { s.db.Close() } func (s *UserSuite) SetupTest() { _, err := s.db.Exec("DELETE FROM users") s.Require().NoError(err) } func (s *UserSuite) TestCreate() { u, err := s.svc.Create(context.Background(), "bob@example.com") s.Require().NoError(err) s.Equal("bob@example.com", u.Email) } func TestUserSuite(t *testing.T) { suite.Run(t, new(UserSuite)) } ``` ### testify/mock Use `mock.Mock` for dynamic expectations with call counting. ```go import "github.com/stretchr/testify/mock" type MockStore struct { mock.Mock } func (m *MockStore) GetUser(ctx context.Context, id int64) (*User, error) { args := m.Called(ctx, id) return args.Get(0).(*User), args.Error(1) } func TestWithMock(t *testing.T) { store := new(MockStore) store.On("GetUser", mock.Anything, int64(1)). Return(&User{ID: 1, Name: "Alice"}, nil) svc := NewUserService(store) user, err := svc.GetUser(context.Background(), 1) require.NoError(t, err) assert.Equal(t, "Alice", user.Name) store.AssertExpectations(t) } ``` --- ## 5. httptest ### Test HTTP Handlers Directly ```go import "net/http/httptest" func TestGetUserHandler(t *testing.T) { store := &mockUserStore{ getUser: func(_ context.Context, id int64) (*User, error) { return &User{ID: id, Name: "Alice"}, nil }, } h := NewHandler(store) req := httptest.NewRequest(http.MethodGet, "/users/1", nil) w := httptest.NewRecorder() h.ServeHTTP(w, req) resp := w.Result() defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status %d, want 200", resp.StatusCode) } var got User if err := json.NewDecoder(resp.Body).Decode(&got); err != nil { t.Fatalf("decoding response: %v", err) } if got.Name != "Alice" { t.Errorf("name %q, want Alice", got.Name) } } ``` ### Test HTTP Clients Against a Real Server ```go func TestAPIClient(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/users/42" { t.Errorf("unexpected path: %s", r.URL.Path) } w.Header().Set("Content-Type", "application/json") fmt.Fprintln(w, `{"id":42,"name":"Bob"}`) })) defer srv.Close() client := NewAPIClient(srv.URL) user, err := client.GetUser(context.Background(), 42) if err != nil { t.Fatalf("GetUser: %v", err) } if user.Name != "Bob" { t.Errorf("name %q, want Bob", user.Name) } } // TLS variant func TestAPIClientTLS(t *testing.T) { srv := httptest.NewTLSServer(myHandler) defer srv.Close() client := srv.Client() // pre-configured to trust the test certificate resp, err := client.Get(srv.URL + "/health") // ... } ``` --- ## 6. Benchmarks Functions named `BenchmarkXxx` receive `*testing.B`. Run with `go test -bench=. -benchmem`. ```go func BenchmarkEncode(b *testing.B) { user := &User{ID: 1, Name: "Alice", Email: "alice@example.com"} b.ReportAllocs() // show allocations per op b.ResetTimer() // exclude setup time for b.Loop() { // Go 1.24+; use i := 0; i < b.N; i++ for older versions if _, err := json.Marshal(user); err != nil { b.Fatal(err) } } } // Sub-benchmarks compare implementations func BenchmarkEncoding(b *testing.B) { user := &User{ID: 1, Name: "Alice", Email: "alice@example.com"} b.Run("json/stdlib", func(b *testing.B) { for b.Loop() { json.Marshal(user) } }) b.Run("json/sonic", func(b *testing.B) { for b.Loop() { sonic.Marshal(user) } }) } // Parallel benchmark func BenchmarkEncodeParallel(b *testing.B) { user := &User{ID: 1, Name: "Alice"} b.RunParallel(func(pb *testing.PB) { for pb.Next() { json.Marshal(user) } }) } ``` Run and compare: `go test -bench=BenchmarkEncoding -benchmem -count=5 | tee new.txt && benchstat old.txt new.txt`. --- ## 7. Fuzz Testing Fuzz tests find inputs that crash your code. Run normally as unit tests; enable fuzzing with `-fuzz`. ```go func FuzzParseURL(f *testing.F) { // Seed the corpus with known-good inputs f.Add("https://example.com/path?q=1") f.Add("http://localhost:8080") f.Add("") f.Add("not-a-url") f.Fuzz(func(t *testing.T, raw string) { // Must not panic u, err := ParseURL(raw) if err != nil { return // errors are acceptable } // Round-trip property: re-parsing the output must succeed reparsed, err := ParseURL(u.String()) if err != nil { t.Errorf("round-trip failed for %q: %v", u.String(), err) } if reparsed.String() != u.String() { t.Errorf("round-trip mismatch: %q != %q", reparsed.String(), u.String()) } }) } ``` Run fuzzing: `go test -fuzz=FuzzParseURL -fuzztime=30s`. Failing inputs are saved to `testdata/fuzz/FuzzParseURL/`. Reproduce: `go test -run=FuzzParseURL/testdata/fuzz/FuzzParseURL/<id>`. --- ## 8. Integration Tests ### Build Tags Guard integration tests with a build tag so `go test ./...` skips them by default. ```go //go:build integration package store_test import ( "testing" // ... ) func TestPostgresUserStore(t *testing.T) { dsn := os.Getenv("TEST_DSN") if dsn == "" { t.Skip("TEST_DSN not set") } // ... } ``` Run: `go test -tags integration ./...` ### testcontainers-go Spin up real databases in Docker for integration tests. ```go //go:build integration func TestWithPostgres(t *testing.T) { 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"). WithOccurrence(2)), ) if err != nil { t.Fatalf("starting postgres: %v", err) } t.Cleanup(func() { container.Terminate(ctx) }) dsn, err := container.ConnectionString(ctx, "sslmode=disable") if err != nil { t.Fatal(err) } db, err := sql.Open("postgres", dsn) if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) // Run migrations, then test runMigrations(t, db) store := NewPostgresStore(db) // ... test store methods } ``` --- ## 9. Golden Files Golden files store expected output. Re-generate them with `-update`. ```go var update = flag.Bool("update", false, "update golden files") func TestRenderMarkdown(t *testing.T) { input, err := os.ReadFile(filepath.Join("testdata", "input.md")) if err != nil { t.Fatal(err) } got := RenderMarkdown(input) golden := filepath.Join("testdata", "golden", "output.html") if *update { err := os.WriteFile(golden, got, 0644) if err != nil { t.Fatal(err) } return } want, err := os.ReadFile(golden) if err != nil { t.Fatal(err) } if !bytes.Equal(got, want) { t.Errorf("output mismatch (-want +got):\n%s", cmp.Diff(string(want), string(got))) } } ``` Run `go test -run=TestRenderMarkdown -update` to regenerate, then commit the golden files. --- ## 10. Test Fixtures and TestMain ### TestMain for Global Setup ```go func TestMain(m *testing.M) { // Setup: runs once before any test db, err := setupTestDatabase() if err != nil { fmt.Fprintf(os.Stderr, "setup: %v\n", err) os.Exit(1) } globalDB = db // Run tests code := m.Run() // Teardown: runs once after all tests db.Close() os.Exit(code) } ``` ### Per-Test Setup with t.Cleanup Prefer `t.Cleanup` over `defer` in test helpers; it composes across multiple helpers cleanly. ```go func prepareUser(t *testing.T, db *sql.DB, email string) *User { t.Helper() u, err := db.CreateUser(context.Background(), email) if err != nil { t.Fatalf("creating user: %v", err) } t.Cleanup(func() { if err := db.DeleteUser(context.Background(), u.ID); err != nil { t.Logf("cleanup: deleting user %d: %v", u.ID, err) } }) return u } ``` --- ## 11. Race Detection Enable with `go test -race ./...`. The race detector adds ~5-10x overhead; use it in CI. ### Common Race: Shared State in Goroutines ```go // RACE: multiple goroutines write to results without synchronization func badCollect(items []Item) []Result { results := make([]Result, 0, len(items)) var wg sync.WaitGroup for _, item := range items { wg.Add(1) go func(it Item) { defer wg.Done() results = append(results, process(it)) // DATA RACE }(item) } wg.Wait() return results } // FIXED: preallocate by index func goodCollect(items []Item) []Result { results := make([]Result, len(items)) var wg sync.WaitGroup for i, item := range items { wg.Add(1) go func(i int, it Item) { defer wg.Done() results[i] = process(it) // safe: each goroutine owns its index }(i, item) } wg.Wait() return results } ``` ### Common Race: Closing Over Loop Variables (pre-Go 1.22) ```go // RACE in Go < 1.22 for _, url := range urls { go func() { fetch(url) // captures loop variable by reference }() } // FIXED for _, url := range urls { url := url // shadow with local copy go func() { fetch(url) }() } ``` --- ## 12. Coverage ```bash # Generate coverage profile go test -coverprofile=coverage.out ./... # View summary by package go tool cover -func=coverage.out # Open interactive HTML report go tool cover -html=coverage.out # Enforce a minimum threshold in CI go test -coverprofile=coverage.out ./... go tool cover -func=coverage.out | awk '/^total:/ {pct=$3+0; if (pct < 80) {print "coverage "$pct"% below 80%"; exit 1}}' ``` Target 80% coverage for business logic. Avoid chasing 100%: generated code, main functions, and deliberate error paths that only trigger under hardware failure are not worth testing directly.
-
-
scripts
-
.gitkeep 0 B · in bundle
-
-
SKILL.md 9.4 KB
--- name: go-ops description: "Go development patterns, concurrency, error handling, testing, and project structure. Use for: golang, go, goroutine, channel, context, errgroup, go test, go mod, go build, interface, generics, table-driven tests, worker pool, sync.Mutex, sync.WaitGroup, pprof, go vet, golangci-lint, go workspace, functional options, middleware, http handler." license: MIT allowed-tools: "Read Write Bash" metadata: author: claude-mods related-skills: docker-ops, ci-cd-ops, api-design-ops, testing-ops --- # Go Operations Comprehensive Go skill covering idiomatic patterns, concurrency, and production practices. ## Module Quick Start ```bash # New module go mod init github.com/user/project # Add dependency go get github.com/lib/pq@latest # Tidy (remove unused, add missing) go mod tidy # Vendor dependencies go mod vendor # Workspace (multi-module) go work init ./api ./shared go work use ./cli ``` ## Error Handling Decision Tree ``` What kind of error? │ ├─ Known, expected condition (e.g. "not found") │ └─ Sentinel error: var ErrNotFound = errors.New("not found") │ └─ Caller checks: errors.Is(err, ErrNotFound) │ ├─ Need to carry structured data (status code, field name) │ └─ Custom error type: type ValidationError struct { Field, Message string } │ └─ Implement Error() string │ └─ Caller checks: errors.As(err, &validErr) │ ├─ Adding context to an existing error │ └─ Wrap: fmt.Errorf("load config: %w", err) │ └─ Preserves original for Is/As checks │ ├─ Truly unrecoverable (corrupted state, programmer bug) │ └─ panic("invariant violated: ...") │ └─ Almost never in library code │ └─ Multiple errors from concurrent work └─ errors.Join(err1, err2) or multierr package ``` ### Error Wrapping Convention ```go // Add context at each layer, don't repeat the function name func LoadUser(id int) (*User, error) { row, err := db.Query("SELECT ...", id) if err != nil { return nil, fmt.Errorf("load user %d: %w", id, err) } // ... } ``` ## Concurrency Decision Tree ``` What's the concurrency pattern? │ ├─ Run N independent tasks, collect results │ └─ errgroup.Group (cancels on first error) │ ├─ Fire-and-forget background work │ └─ go func() with context for cancellation │ └─ ALWAYS handle the error or log it │ ├─ Producer/consumer pipeline │ └─ Channels (buffered for throughput) │ └─ Close channel when producer is done │ ├─ Rate-limited concurrent work │ └─ Semaphore: make(chan struct{}, maxConcurrency) │ ├─ Shared mutable state │ └─ sync.Mutex or sync.RWMutex │ └─ Prefer channels if the state is simple │ ├─ One-time initialization │ └─ sync.Once │ └─ Wait for N goroutines to finish (no error collection) └─ sync.WaitGroup ``` ### errgroup Quick Start ```go import "golang.org/x/sync/errgroup" g, ctx := errgroup.WithContext(ctx) g.SetLimit(10) // max 10 concurrent goroutines for _, url := range urls { g.Go(func() error { return fetch(ctx, url) }) } if err := g.Wait(); err != nil { return fmt.Errorf("fetch urls: %w", err) } ``` **Deep dive**: Load `./references/concurrency.md` for worker pools, fan-out/fan-in, pipeline patterns, context best practices. ## Interface Design ``` Accept interfaces, return structs. ``` ```go // Good: function accepts interface func Process(r io.Reader) error { ... } // Good: return concrete type func NewServer(cfg Config) *Server { ... } // Bad: returning interface (hides implementation, prevents extension) func NewServer(cfg Config) ServerInterface { ... } ``` ### Common Stdlib Interfaces | Interface | Methods | Use For | |-----------|---------|---------| | `io.Reader` | `Read([]byte) (int, error)` | Any byte source | | `io.Writer` | `Write([]byte) (int, error)` | Any byte sink | | `io.Closer` | `Close() error` | Resource cleanup | | `fmt.Stringer` | `String() string` | String representation | | `error` | `Error() string` | Error values | | `sort.Interface` | `Len, Less, Swap` | Custom sorting | | `http.Handler` | `ServeHTTP(w, r)` | HTTP handlers | | `encoding.BinaryMarshaler` | `MarshalBinary() ([]byte, error)` | Binary encoding | ### Functional Options Pattern ```go type Option func(*Server) func WithPort(port int) Option { return func(s *Server) { s.port = port } } func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } } func NewServer(opts ...Option) *Server { s := &Server{port: 8080, timeout: 30 * time.Second} // defaults for _, opt := range opts { opt(s) } return s } // Usage srv := NewServer(WithPort(9090), WithTimeout(5*time.Second)) ``` **Deep dive**: Load `./references/interfaces-generics.md` for generics, type constraints, embedding, type assertions. ## Testing Quick Reference ```go // Table-driven test func TestAdd(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"positive", 1, 2, 3}, {"zero", 0, 0, 0}, {"negative", -1, -2, -3}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := Add(tt.a, tt.b) if got != tt.expected { t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.expected) } }) } } ``` ```bash # Run tests go test ./... # With coverage go test -cover -coverprofile=coverage.out ./... go tool cover -html=coverage.out # Run specific test go test -run TestAdd ./pkg/math/ # Benchmarks go test -bench=. -benchmem ./... # Race detector go test -race ./... # Fuzz testing go test -fuzz=FuzzParse ./... ``` **Deep dive**: Load `./references/testing.md` for mocking with interfaces, httptest, testcontainers, golden files. ## Common Gotchas | Gotcha | Why | Fix | |--------|-----|-----| | Nil slice vs empty slice | `var s []int` is nil, `s := []int{}` is empty. `json.Marshal` gives `null` vs `[]` | Use `make([]int, 0)` or `[]int{}` if JSON matters | | Goroutine leak | Goroutine blocked on channel with no reader/writer | Use `context.WithCancel`, always provide exit path | | Defer in loop | Deferred calls don't run until function returns | Wrap loop body in a closure or use explicit cleanup | | Interface nil pitfall | `(*MyType)(nil)` assigned to `error` interface is not `== nil` | Return `nil` explicitly, not a nil typed pointer | | Range variable capture | Loop var reused (pre-Go 1.22) | Use `go func(v T) { ... }(v)` or upgrade to Go 1.22+ | | String concatenation in loop | O(n^2) allocation | Use `strings.Builder` | | `sync.WaitGroup` Add after Go | Race condition | Call `wg.Add(1)` before `go func()` | | Unbuffered channel deadlock | Send/receive must happen concurrently | Use buffered channel or separate goroutines | | `map` not safe for concurrent use | Race condition, may crash | Use `sync.Mutex` or `sync.Map` | ## Project Structure ``` project/ ├── cmd/ │ ├── api/main.go # Entry points │ └── worker/main.go ├── internal/ # Private packages │ ├── handler/ │ ├── service/ │ └── repository/ ├── pkg/ # Public packages (optional) ├── go.mod ├── go.sum ├── Makefile # or justfile └── .golangci.yml ``` **Deep dive**: Load `./references/project-structure.md` for workspace mode, build tags, ldflags, linting config. ## Performance Quick Reference ```bash # CPU profile go test -cpuprofile=cpu.prof -bench=. ./... go tool pprof cpu.prof # Memory profile go test -memprofile=mem.prof -bench=. ./... go tool pprof -alloc_space mem.prof # Trace go test -trace=trace.out ./... go tool trace trace.out # Escape analysis go build -gcflags='-m' ./... ``` | Optimization | When | Pattern | |-------------|------|---------| | Pre-allocate slices | Known size | `make([]T, 0, n)` | | `strings.Builder` | String concatenation | `var b strings.Builder` | | `sync.Pool` | Frequent alloc/free of same type | `pool.Get()` / `pool.Put()` | | Struct field alignment | Memory-sensitive | Group fields by size (largest first) | | Buffer reuse | I/O-heavy | `bufio.NewReaderSize(r, 64*1024)` | **Deep dive**: Load `./references/performance.md` for pprof walkthrough, benchmarking patterns, escape analysis. ## Reference Files Load these for deep-dive topics. Each is self-contained. | Reference | When to Load | |-----------|-------------| | `./references/concurrency.md` | Goroutines, channels, context, sync primitives, worker pools, pipelines | | `./references/error-handling.md` | Error wrapping, sentinel errors, custom types, multi-error, panic/recover | | `./references/testing.md` | Table tests, mocking, httptest, benchmarks, fuzz, testcontainers, golden files | | `./references/interfaces-generics.md` | Interface design, embedding, type assertions, generics, type constraints | | `./references/project-structure.md` | Standard layout, go.mod, workspaces, build tags, ldflags, golangci-lint | | `./references/performance.md` | pprof, trace, benchmarks, escape analysis, sync.Pool, struct alignment | | `./references/expert-insights.md` | HTTP server (Go 1.22 routing), graceful shutdown, http.Client tuning, JSON helpers | ## See Also - `docker-ops` - Multi-stage builds for Go binaries (scratch/distroless) - `ci-cd-ops` - Go CI pipelines, caching go modules, goreleaser - `testing-ops` - Cross-language testing strategies
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.