Use when writing or debugging non-trivial Go — error handling patterns, goroutine/channel design, interface composition, generics, context propagation, or Go-specific idioms like table-driven tests and functional options.
Scanned 9/9/2026
Install to Claude Code
npx -y skills add kid-sid/codex-spellbook --skill go --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Go?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/kid-sid-codex-spellbook)More formats (shields.io, HTML) on the badges page.
---
name: go
description: Use when writing or debugging non-trivial Go — error handling patterns, goroutine/channel design, interface composition, generics, context propagation, or Go-specific idioms like table-driven tests and functional options.
---
# Go — Advanced Patterns
Language-level patterns for writing correct, idiomatic, performant Go.
## When to Activate
- Designing error types, wrapping, and sentinel errors
- Structuring goroutines, channels, and `sync` primitives correctly
- Propagating `context.Context` for cancellation and deadlines
- Composing interfaces and embedding types
- Using generics (`any`, constraints, type parameters) appropriately
- Writing table-driven tests, benchmarks, or fuzz targets
- Applying functional options, builder patterns, or the options struct pattern
- Structuring packages, modules, and internal vs exported APIs
---
## Error Handling
### Wrapping and unwrapping
```go
import "errors"
// Wrap to add context — preserves the original for errors.Is/As
if err != nil {
return fmt.Errorf("fetch user %d: %w", id, err)
}
// Sentinel errors — compare with errors.Is (not ==)
var ErrNotFound = errors.New("not found")
var ErrUnauthorized = errors.New("unauthorized")
if errors.Is(err, ErrNotFound) {
// handle not found
}
// Custom error type — use errors.As to extract
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}
var ve *ValidationError
if errors.As(err, &ve) {
log.Printf("invalid field: %s", ve.Field)
}
```
### Multiple return errors
```go
// BAD: ignoring errors
data, _ := os.ReadFile("config.json")
// GOOD: always handle
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("read config: %w", err)
}
// Return early, keep the happy path unindented
func process(id string) (*Result, error) {
user, err := db.GetUser(id)
if err != nil {
return nil, fmt.Errorf("get user: %w", err)
}
orders, err := db.GetOrders(id)
if err != nil {
return nil, fmt.Errorf("get orders: %w", err)
}
return &Result{User: user, Orders: orders}, nil
}
```
---
## Goroutines and Channels
### Goroutine lifecycle — always have an exit strategy
```go
// BAD: goroutine leaks — no way to stop it
go func() {
for {
process()
}
}()
// GOOD: context-driven shutdown
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return // channel closed
}
process(job)
}
}
}
```
### Fan-out / fan-in
```go
func fanOut(ctx context.Context, in <-chan int, workers int) <-chan Result {
out := make(chan Result)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for v := range in {
select {
case out <- compute(v):
case <-ctx.Done():
return
}
}
}()
}
go func() {
wg.Wait()
close(out) // signal downstream that all workers are done
}()
return out
}
```
### Channel patterns
```go
// Done channel — broadcast shutdown to many goroutines
done := make(chan struct{})
close(done) // unblocks ALL receivers simultaneously
// Buffered channel as semaphore — limit concurrency
sem := make(chan struct{}, 10)
sem <- struct{}{} // acquire
defer func() { <-sem }() // release
// Pipeline stage
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
// Select with default — non-blocking send/receive
select {
case ch <- value:
// sent
default:
// channel full — drop or handle
}
```
### `sync` primitives
```go
// Mutex — protect shared state
type SafeCounter struct {
mu sync.Mutex
count map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.count[key]++
}
// RWMutex — concurrent reads, exclusive writes
type Cache struct {
mu sync.RWMutex
store map[string]string
}
func (c *Cache) Get(k string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.store[k]
return v, ok
}
// sync.Once — initialize exactly once (safe for goroutines)
var instance *DB
var once sync.Once
func GetDB() *DB {
once.Do(func() { instance = connect() })
return instance
}
// sync.WaitGroup — wait for a collection of goroutines
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(i Item) {
defer wg.Done()
process(i)
}(item)
}
wg.Wait()
```
---
## Context
```go
// Always accept context as the first parameter in public functions
func FetchUser(ctx context.Context, id string) (*User, error) { ... }
// Propagate — never store context in structs
type Service struct{ db *DB } // GOOD — context passed per call
func (s *Service) Get(ctx context.Context, id string) (*User, error) { ... }
// Deadline / timeout
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() // always defer cancel — releases resources even on success
// Cancellation
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
cancel() // signal worker to stop
// Value — only for request-scoped data (trace IDs, auth tokens), not config
type ctxKey string
const requestIDKey ctxKey = "requestID"
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func RequestID(ctx context.Context) string {
v, _ := ctx.Value(requestIDKey).(string)
return v
}
// Check cancellation in long loops
for _, item := range largeSlice {
if ctx.Err() != nil {
return ctx.Err()
}
process(item)
}
```
---
## Interfaces and Embedding
### Interface design — small, composable
```go
// BAD: fat interface — hard to implement, hard to test
type Storage interface {
Get(id string) (*User, error)
Save(u *User) error
Delete(id string) error
List(filter Filter) ([]*User, error)
Search(q string) ([]*User, error)
Count() (int, error)
}
// GOOD: small, focused interfaces
type UserGetter interface {
GetUser(ctx context.Context, id string) (*User, error)
}
type UserSaver interface {
SaveUser(ctx context.Context, u *User) error
}
// Compose only where needed
type UserStore interface {
UserGetter
UserSaver
}
// Accept interfaces, return structs
func NewService(store UserGetter) *Service { ... } // testable
func NewPostgresStore(db *sql.DB) *PostgresStore { ... } // concrete return
```
### Embedding
```go
// Struct embedding — promotes fields and methods
type Animal struct{ Name string }
func (a Animal) Speak() string { return a.Name }
type Dog struct {
Animal // promoted: dog.Name, dog.Speak()
Breed string
}
// Interface embedding
type ReadWriter interface {
io.Reader
io.Writer
}
// Embedding to extend without inheriting
type LoggedStore struct {
Store // delegates all Store methods
log *slog.Logger
}
func (ls *LoggedStore) GetUser(ctx context.Context, id string) (*User, error) {
u, err := ls.Store.GetUser(ctx, id) // delegate
ls.log.Info("get user", "id", id, "err", err)
return u, err
}
```
---
## Functional Options Pattern
Preferred over long constructor signatures or config structs that need zero values to be meaningful.
```go
type Server struct {
host string
port int
timeout time.Duration
logger *slog.Logger
}
type Option func(*Server)
func WithPort(p int) Option { return func(s *Server) { s.port = p } }
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func WithLogger(l *slog.Logger) Option { return func(s *Server) { s.logger = l } }
func NewServer(host string, opts ...Option) *Server {
s := &Server{host: host, port: 8080, timeout: 30 * time.Second}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
srv := NewServer("localhost",
WithPort(9090),
WithTimeout(10*time.Second),
WithLogger(slog.Default()),
)
```
---
## Generics
```go
// Type constraint — built-in or custom
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Sum[T Number](items []T) T {
var total T
for _, v := range items {
total += v
}
return total
}
// Generic data structures
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}
// Map / Filter helpers
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func Filter[T any](slice []T, fn func(T) bool) []T {
var result []T
for _, v := range slice {
if fn(v) {
result = append(result, v)
}
}
return result
}
```
---
## HTTP Patterns (net/http)
```go
// Handler with dependencies — use a method, not a closure
type Handler struct {
svc *UserService
log *slog.Logger
}
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // Go 1.22+ built-in path params
user, err := h.svc.Get(r.Context(), id)
if err != nil {
if errors.Is(err, ErrNotFound) {
http.Error(w, "not found", http.StatusNotFound)
return
}
h.log.Error("get user", "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// Routing (Go 1.22+)
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", h.GetUser)
mux.HandleFunc("POST /users", h.CreateUser)
// Middleware chaining
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("request", "method", r.Method, "path", r.URL.Path, "dur", time.Since(start))
})
}
srv := &http.Server{
Addr: ":8080",
Handler: logging(mux),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
```
---
## Testing
### Table-driven tests
```go
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
want int
}{
{"positive", 1, 2, 3},
{"negative", -1, -2, -3},
{"zero", 0, 0, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := Add(tc.a, tc.b)
if got != tc.want {
t.Errorf("Add(%d, %d) = %d, want %d", tc.a, tc.b, got, tc.want)
}
})
}
}
```
### Test helpers and cleanup
```go
func setupDB(t *testing.T) *sql.DB {
t.Helper() // marks this as a helper — errors show caller's line
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("open db: %v", err)
}
t.Cleanup(func() { db.Close() }) // runs after test, even on failure
return db
}
```
### Benchmarks and fuzz tests
```go
func BenchmarkProcess(b *testing.B) {
data := generateData()
b.ResetTimer() // exclude setup time
for i := 0; i < b.N; i++ {
Process(data)
}
}
// Run: go test -bench=. -benchmem
func FuzzParse(f *testing.F) {
f.Add("valid input") // seed corpus
f.Fuzz(func(t *testing.T, s string) {
_, err := Parse(s)
// must not panic — error is fine
_ = err
})
}
// Run: go test -fuzz=FuzzParse
```
---
## Package and Module Conventions
| Pattern | Rule |
|---|---|
| Package names | Short, lowercase, no underscores: `store`, `auth`, `httputil` |
| Exported names | Self-documenting without package prefix: `store.User` not `store.StoreUser` |
| `internal/` | Enforces package privacy — only importable by parent module |
| `cmd/` | One sub-package per binary entry point |
| Error variables | Prefix with `Err`: `var ErrNotFound = errors.New(...)` |
| Interface location | Define interfaces in the **consumer** package, not the implementer |
| `init()` | Avoid — prefer explicit initialization in `main` or constructors |
```
myapp/
├── cmd/
│ └── server/main.go # entry point
├── internal/
│ ├── auth/ # private to this module
│ └── store/
├── pkg/ # reusable, importable by others
│ └── httputil/
└── go.mod
```
---
## Common Gotchas
```go
// Loop variable capture (Go < 1.22) — goroutine closes over the same variable
for _, v := range items {
go func() { process(v) }() // BAD pre-1.22: all goroutines see the final v
go func(v Item) { process(v) }(v) // GOOD: pass as argument
}
// Go 1.22+: each iteration gets its own copy automatically
// Nil interface != nil pointer
var p *MyType = nil
var i interface{} = p
i == nil // false — interface has type info even if value is nil
// Return a plain nil to get a nil interface:
func getError() error { return nil } // not return (*MyError)(nil)
// Slice append aliasing
a := []int{1, 2, 3}
b := a[:2]
b = append(b, 99) // may overwrite a[2] if cap allows
// Use a[low:high:max] to control capacity and prevent aliasing:
b = a[:2:2] // cap=2, so append always allocates a new backing array
// Map zero value is nil — must initialize before writing
var m map[string]int
m["x"] = 1 // panic: assignment to entry in nil map
m = make(map[string]int)
m["x"] = 1 // ok
// defer in a loop — deferred until function returns, not loop iteration
for _, f := range files {
f, _ := os.Open(f)
defer f.Close() // BAD: all closes happen at function end
}
// GOOD: wrap in a closure or helper function
for _, name := range files {
func() {
f, _ := os.Open(name)
defer f.Close()
process(f)
}()
}
```
---
## Performance Tips
| Technique | When to use |
|---|---|
| Pre-allocate slices | `make([]T, 0, knownLen)` — avoids repeated reallocation |
| Pre-allocate maps | `make(map[K]V, knownLen)` — reduces rehashing |
| `strings.Builder` | Building strings in a loop — never `+=` in a loop |
| `sync.Pool` | Reuse short-lived allocations (buffers, scratch objects) |
| Avoid interface{} in hot paths | Boxing/unboxing costs allocation; use generics or concrete types |
| `//go:noescape` / `unsafe` | Only after profiling with `pprof` |
| Benchmark first | `go test -bench=. -benchmem -cpuprofile=cpu.out` |
---
## Red Flags
- **Goroutine without a stop condition** — every goroutine must have a way to exit (context cancellation, channel close, or done signal); goroutine leaks accumulate and crash servers under load
- **Storing `context.Context` in a struct** — context is request-scoped and must be passed as the first function parameter; storing it bypasses cancellation and makes the struct un-testable
- **`errors.New` compared with `==`** — sentinel errors must use `errors.Is` because wrapping breaks `==`; define `var ErrX = errors.New(...)` and always check with `errors.Is`
- **Shadowing `err` with `:=` in nested scope** — `if err := ...; err != nil` inside a block creates a new `err` that shadows the outer one; outer error is silently unchanged
- **Nil pointer returned as non-nil interface** — returning `(*ConcreteType)(nil)` as an `error` or `interface{}` produces a non-nil interface; always return untyped `nil`
- **`defer` inside a loop** — deferred calls run at function return, not loop end; open file handles accumulate; wrap the loop body in a closure or helper function
- **Fat interfaces defined in the implementer package** — interfaces belong in the consumer package; large interfaces make mocking painful and couple packages unnecessarily
- **Accessing a nil map** — reading is safe (returns zero value), writing panics; always initialize maps with `make`
## Checklist
- [ ] Every `error` return is either handled or explicitly propagated with `fmt.Errorf("...: %w", err)`
- [ ] Sentinel errors use `errors.Is` / `errors.As`, never `==`
- [ ] Every goroutine has a documented exit path (context, channel close, done signal)
- [ ] `context.Context` is the first parameter of every function that does I/O or calls other services
- [ ] `cancel()` from `WithTimeout` / `WithCancel` is deferred immediately after creation
- [ ] Interfaces are defined in the consumer package and kept small (1-3 methods)
- [ ] `sync.WaitGroup.Add()` is called before launching the goroutine, not inside it
- [ ] Slices passed to goroutines are copied or ownership is clearly transferred
- [ ] Maps are initialized with `make` before any write
- [ ] Table-driven tests cover happy path, zero value, and at least one error case
- [ ] Benchmarks use `b.ResetTimer()` after setup and `b.ReportAllocs()`
- [ ] `t.Helper()` is called in every test helper function
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!