Go Expert
Clear is better than clever. Handle every error explicitly, make the zero value useful, and give every goroutine an owner and an exit. "Share memory by communicating," but a Mutex is fine for protecting state.
When to Use
- Writing or refactoring Go.
- Designing concurrent code (goroutines, channels,
context, select).
- Data races, deadlocks, goroutine leaks, or context propagation bugs.
- Package layout, interfaces, and standard-library usage.
When NOT to Use
- Other languages → the relevant language skill.
- System-level architecture →
software-architect.
Core Principles
1. Errors are explicit values
- Check immediately:
if err != nil { return fmt.Errorf("doing X: %w", err) }. Wrap with %w to preserve the chain; inspect with errors.Is (sentinel) / errors.As (typed).
- Add context at each layer ("opening config: …") but don't double-log the same error. Return errors up; log once at the boundary.
- Panic only for truly unrecoverable programmer errors, never for normal control flow.
2. Interfaces & types
- Accept interfaces, return concrete types. Keep interfaces small (often one method) and define them in the consumer package, not the producer.
- Make the zero value useful (
sync.Mutex, bytes.Buffer work unboxed). Use defer for cleanup (close/unlock) right after acquisition.
3. Concurrency with ownership
- Every goroutine needs a clear lifetime and exit path — leaks are silent. Use
sync.WaitGroup or errgroup to wait, and bound concurrency (errgroup.SetLimit, worker pool, semaphore).
context.Context is the first parameter for anything cancelable/IO-bound; honor ctx.Done(). Never store a Context in a struct.
- Channels for handoff/signaling;
Mutex for protecting shared state. Close a channel from the sender, once. Prefer select with ctx for cancelable waits.
4. Tooling & structure
gofmt (non-negotiable), go vet, golangci-lint. Always test concurrent code with -race.
- Organize by capability; keep private code under
internal/. Avoid premature interfaces and needless abstraction.
Common Mistakes
- Loop variable capture in goroutines/closures — historically
for i, v := range needed i, v := i, v; fixed in Go 1.22+, but know your version.
- Ignoring errors (
_ = f()) or only logging without returning.
- Goroutine leak: a goroutine blocked on a channel/
ctx that never fires — always provide cancellation.
WaitGroup.Add inside the goroutine → race; call Add before go.
- Mutating a map concurrently → fatal race; guard with
Mutex or use sync.Map for the right pattern.
- Returning an interface holding a nil pointer →
err != nil is true unexpectedly (typed-nil trap).
- Unbounded goroutine spawning per request → resource exhaustion.
Examples
Bounded concurrency with errgroup + context
func fetchAll(ctx context.Context, urls []string) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // cap concurrency
out := make([]string, len(urls))
for i, u := range urls { // Go 1.22+: no manual capture needed
i, u := i, u
g.Go(func() error {
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("get %s: %w", u, err)
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
out[i] = string(b)
return err
})
}
return out, g.Wait() // first error cancels the rest via ctx
}
Sentinel error inspection
if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound }
See Also
performance-expert — pprof, escape analysis, allocation reduction.
testing-expert — table-driven tests and -race.
docker-expert — tiny static images (FROM scratch/distroless).
rust-expert — alternative ownership-based concurrency model.
1---2name: go-expert3description: Expert idiomatic Go: concurrency, interfaces, error handling, and the standard library. Trigger keywords: Go, golang, goroutine, channel, context, select, interface, error wrapping, errors.Is, defer, sync, Mutex, WaitGroup, errgroup, race, pprof. Use for writing/refactoring Go, designing concurrency, or fixing races, leaks, and deadlocks.4---56# Go Expert78> Clear is better than clever. Handle every error explicitly, make the zero value useful, and give every goroutine an owner and an exit. "Share memory by communicating," but a `Mutex` is fine for protecting state.910## When to Use11- Writing or refactoring Go.12- Designing concurrent code (goroutines, channels, `context`, `select`).13- Data races, deadlocks, goroutine leaks, or context propagation bugs.14- Package layout, interfaces, and standard-library usage.1516## When NOT to Use17- Other languages → the relevant language skill.18- System-level architecture → `software-architect`.1920## Core Principles2122### 1. Errors are explicit values23- Check immediately: `if err != nil { return fmt.Errorf("doing X: %w", err) }`. Wrap with `%w` to preserve the chain; inspect with `errors.Is` (sentinel) / `errors.As` (typed).24- Add context at each layer ("opening config: …") but don't double-log the same error. Return errors up; log once at the boundary.25- Panic only for truly unrecoverable programmer errors, never for normal control flow.2627### 2. Interfaces & types28- **Accept interfaces, return concrete types.** Keep interfaces small (often one method) and define them in the **consumer** package, not the producer.29- Make the **zero value useful** (`sync.Mutex`, `bytes.Buffer` work unboxed). Use `defer` for cleanup (close/unlock) right after acquisition.3031### 3. Concurrency with ownership32- Every goroutine needs a clear lifetime and exit path — leaks are silent. Use `sync.WaitGroup` or `errgroup` to wait, and **bound** concurrency (`errgroup.SetLimit`, worker pool, semaphore).33- `context.Context` is the first parameter for anything cancelable/IO-bound; honor `ctx.Done()`. Never store a Context in a struct.34- Channels for handoff/signaling; `Mutex` for protecting shared state. Close a channel from the **sender**, once. Prefer `select` with `ctx` for cancelable waits.3536### 4. Tooling & structure37- `gofmt` (non-negotiable), `go vet`, `golangci-lint`. **Always test concurrent code with `-race`.**38- Organize by capability; keep private code under `internal/`. Avoid premature interfaces and needless abstraction.3940## Common Mistakes41- **Loop variable capture** in goroutines/closures — historically `for i, v := range` needed `i, v := i, v`; fixed in Go 1.22+, but know your version.42- **Ignoring errors** (`_ = f()`) or only logging without returning.43- **Goroutine leak**: a goroutine blocked on a channel/`ctx` that never fires — always provide cancellation.44- **`WaitGroup.Add` inside the goroutine** → race; call `Add` before `go`.45- **Mutating a map concurrently** → fatal race; guard with `Mutex` or use `sync.Map` for the right pattern.46- **Returning an interface holding a nil pointer** → `err != nil` is true unexpectedly (typed-nil trap).47- **Unbounded goroutine spawning** per request → resource exhaustion.4849## Examples5051**Bounded concurrency with errgroup + context**52```go53func fetchAll(ctx context.Context, urls []string) ([]string, error) {54 g, ctx := errgroup.WithContext(ctx)55 g.SetLimit(10) // cap concurrency56 out := make([]string, len(urls))57 for i, u := range urls { // Go 1.22+: no manual capture needed58 i, u := i, u59 g.Go(func() error {60 req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)61 resp, err := http.DefaultClient.Do(req)62 if err != nil {63 return fmt.Errorf("get %s: %w", u, err)64 }65 defer resp.Body.Close()66 b, err := io.ReadAll(resp.Body)67 out[i] = string(b)68 return err69 })70 }71 return out, g.Wait() // first error cancels the rest via ctx72}73```7475**Sentinel error inspection**76```go77if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound }78```7980## See Also81- `performance-expert` — `pprof`, escape analysis, allocation reduction.82- `testing-expert` — table-driven tests and `-race`.83- `docker-expert` — tiny static images (`FROM scratch`/distroless).84- `rust-expert` — alternative ownership-based concurrency model.