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, goreleaser
testing-ops - Cross-language testing strategies
1---2name: go-ops3description: "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."4license: MIT5allowed-tools: "Read Write Bash"6metadata:7 author: claude-mods8 related-skills: docker-ops, ci-cd-ops, api-design-ops, testing-ops9---1011# Go Operations1213Comprehensive Go skill covering idiomatic patterns, concurrency, and production practices.1415## Module Quick Start1617```bash18# New module19go mod init github.com/user/project2021# Add dependency22go get github.com/lib/pq@latest2324# Tidy (remove unused, add missing)25go mod tidy2627# Vendor dependencies28go mod vendor2930# Workspace (multi-module)31go work init ./api ./shared32go work use ./cli33```3435## Error Handling Decision Tree3637```38What kind of error?39│40├─ Known, expected condition (e.g. "not found")41│ └─ Sentinel error: var ErrNotFound = errors.New("not found")42│ └─ Caller checks: errors.Is(err, ErrNotFound)43│44├─ Need to carry structured data (status code, field name)45│ └─ Custom error type: type ValidationError struct { Field, Message string }46│ └─ Implement Error() string47│ └─ Caller checks: errors.As(err, &validErr)48│49├─ Adding context to an existing error50│ └─ Wrap: fmt.Errorf("load config: %w", err)51│ └─ Preserves original for Is/As checks52│53├─ Truly unrecoverable (corrupted state, programmer bug)54│ └─ panic("invariant violated: ...")55│ └─ Almost never in library code56│57└─ Multiple errors from concurrent work58 └─ errors.Join(err1, err2) or multierr package59```6061### Error Wrapping Convention6263```go64// Add context at each layer, don't repeat the function name65func LoadUser(id int) (*User, error) {66 row, err := db.Query("SELECT ...", id)67 if err != nil {68 return nil, fmt.Errorf("load user %d: %w", id, err)69 }70 // ...71}72```7374## Concurrency Decision Tree7576```77What's the concurrency pattern?78│79├─ Run N independent tasks, collect results80│ └─ errgroup.Group (cancels on first error)81│82├─ Fire-and-forget background work83│ └─ go func() with context for cancellation84│ └─ ALWAYS handle the error or log it85│86├─ Producer/consumer pipeline87│ └─ Channels (buffered for throughput)88│ └─ Close channel when producer is done89│90├─ Rate-limited concurrent work91│ └─ Semaphore: make(chan struct{}, maxConcurrency)92│93├─ Shared mutable state94│ └─ sync.Mutex or sync.RWMutex95│ └─ Prefer channels if the state is simple96│97├─ One-time initialization98│ └─ sync.Once99│100└─ Wait for N goroutines to finish (no error collection)101 └─ sync.WaitGroup102```103104### errgroup Quick Start105106```go107import "golang.org/x/sync/errgroup"108109g, ctx := errgroup.WithContext(ctx)110g.SetLimit(10) // max 10 concurrent goroutines111112for _, url := range urls {113 g.Go(func() error {114 return fetch(ctx, url)115 })116}117118if err := g.Wait(); err != nil {119 return fmt.Errorf("fetch urls: %w", err)120}121```122123**Deep dive**: Load `./references/concurrency.md` for worker pools, fan-out/fan-in, pipeline patterns, context best practices.124125## Interface Design126127```128Accept interfaces, return structs.129```130131```go132// Good: function accepts interface133func Process(r io.Reader) error { ... }134135// Good: return concrete type136func NewServer(cfg Config) *Server { ... }137138// Bad: returning interface (hides implementation, prevents extension)139func NewServer(cfg Config) ServerInterface { ... }140```141142### Common Stdlib Interfaces143144| Interface | Methods | Use For |145|-----------|---------|---------|146| `io.Reader` | `Read([]byte) (int, error)` | Any byte source |147| `io.Writer` | `Write([]byte) (int, error)` | Any byte sink |148| `io.Closer` | `Close() error` | Resource cleanup |149| `fmt.Stringer` | `String() string` | String representation |150| `error` | `Error() string` | Error values |151| `sort.Interface` | `Len, Less, Swap` | Custom sorting |152| `http.Handler` | `ServeHTTP(w, r)` | HTTP handlers |153| `encoding.BinaryMarshaler` | `MarshalBinary() ([]byte, error)` | Binary encoding |154155### Functional Options Pattern156157```go158type Option func(*Server)159160func WithPort(port int) Option {161 return func(s *Server) { s.port = port }162}163164func WithTimeout(d time.Duration) Option {165 return func(s *Server) { s.timeout = d }166}167168func NewServer(opts ...Option) *Server {169 s := &Server{port: 8080, timeout: 30 * time.Second} // defaults170 for _, opt := range opts {171 opt(s)172 }173 return s174}175176// Usage177srv := NewServer(WithPort(9090), WithTimeout(5*time.Second))178```179180**Deep dive**: Load `./references/interfaces-generics.md` for generics, type constraints, embedding, type assertions.181182## Testing Quick Reference183184```go185// Table-driven test186func TestAdd(t *testing.T) {187 tests := []struct {188 name string189 a, b int190 expected int191 }{192 {"positive", 1, 2, 3},193 {"zero", 0, 0, 0},194 {"negative", -1, -2, -3},195 }196 for _, tt := range tests {197 t.Run(tt.name, func(t *testing.T) {198 got := Add(tt.a, tt.b)199 if got != tt.expected {200 t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.expected)201 }202 })203 }204}205```206207```bash208# Run tests209go test ./...210211# With coverage212go test -cover -coverprofile=coverage.out ./...213go tool cover -html=coverage.out214215# Run specific test216go test -run TestAdd ./pkg/math/217218# Benchmarks219go test -bench=. -benchmem ./...220221# Race detector222go test -race ./...223224# Fuzz testing225go test -fuzz=FuzzParse ./...226```227228**Deep dive**: Load `./references/testing.md` for mocking with interfaces, httptest, testcontainers, golden files.229230## Common Gotchas231232| Gotcha | Why | Fix |233|--------|-----|-----|234| 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 |235| Goroutine leak | Goroutine blocked on channel with no reader/writer | Use `context.WithCancel`, always provide exit path |236| Defer in loop | Deferred calls don't run until function returns | Wrap loop body in a closure or use explicit cleanup |237| Interface nil pitfall | `(*MyType)(nil)` assigned to `error` interface is not `== nil` | Return `nil` explicitly, not a nil typed pointer |238| Range variable capture | Loop var reused (pre-Go 1.22) | Use `go func(v T) { ... }(v)` or upgrade to Go 1.22+ |239| String concatenation in loop | O(n^2) allocation | Use `strings.Builder` |240| `sync.WaitGroup` Add after Go | Race condition | Call `wg.Add(1)` before `go func()` |241| Unbuffered channel deadlock | Send/receive must happen concurrently | Use buffered channel or separate goroutines |242| `map` not safe for concurrent use | Race condition, may crash | Use `sync.Mutex` or `sync.Map` |243244## Project Structure245246```247project/248├── cmd/249│ ├── api/main.go # Entry points250│ └── worker/main.go251├── internal/ # Private packages252│ ├── handler/253│ ├── service/254│ └── repository/255├── pkg/ # Public packages (optional)256├── go.mod257├── go.sum258├── Makefile # or justfile259└── .golangci.yml260```261262**Deep dive**: Load `./references/project-structure.md` for workspace mode, build tags, ldflags, linting config.263264## Performance Quick Reference265266```bash267# CPU profile268go test -cpuprofile=cpu.prof -bench=. ./...269go tool pprof cpu.prof270271# Memory profile272go test -memprofile=mem.prof -bench=. ./...273go tool pprof -alloc_space mem.prof274275# Trace276go test -trace=trace.out ./...277go tool trace trace.out278279# Escape analysis280go build -gcflags='-m' ./...281```282283| Optimization | When | Pattern |284|-------------|------|---------|285| Pre-allocate slices | Known size | `make([]T, 0, n)` |286| `strings.Builder` | String concatenation | `var b strings.Builder` |287| `sync.Pool` | Frequent alloc/free of same type | `pool.Get()` / `pool.Put()` |288| Struct field alignment | Memory-sensitive | Group fields by size (largest first) |289| Buffer reuse | I/O-heavy | `bufio.NewReaderSize(r, 64*1024)` |290291**Deep dive**: Load `./references/performance.md` for pprof walkthrough, benchmarking patterns, escape analysis.292293## Reference Files294295Load these for deep-dive topics. Each is self-contained.296297| Reference | When to Load |298|-----------|-------------|299| `./references/concurrency.md` | Goroutines, channels, context, sync primitives, worker pools, pipelines |300| `./references/error-handling.md` | Error wrapping, sentinel errors, custom types, multi-error, panic/recover |301| `./references/testing.md` | Table tests, mocking, httptest, benchmarks, fuzz, testcontainers, golden files |302| `./references/interfaces-generics.md` | Interface design, embedding, type assertions, generics, type constraints |303| `./references/project-structure.md` | Standard layout, go.mod, workspaces, build tags, ldflags, golangci-lint |304| `./references/performance.md` | pprof, trace, benchmarks, escape analysis, sync.Pool, struct alignment |305| `./references/expert-insights.md` | HTTP server (Go 1.22 routing), graceful shutdown, http.Client tuning, JSON helpers |306307## See Also308309- `docker-ops` - Multi-stage builds for Go binaries (scratch/distroless)310- `ci-cd-ops` - Go CI pipelines, caching go modules, goreleaser311- `testing-ops` - Cross-language testing strategies