Go Development (1.25+)
Core Principles
- Stdlib first: External deps only when justified
- Concrete types: Define interfaces at consumer, return structs
- Composition: Over inheritance, always
- Fail fast: Clear errors with context
- Simple: The obvious solution is usually correct
Quick Patterns
Error Handling
if err := doThing(); err != nil {
return fmt.Errorf("do thing: %w", err)
}
Struct with Options
type Server struct {
addr string
timeout time.Duration
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{addr: addr, timeout: 30 * time.Second}
for _, opt := range opts {
opt(s)
}
return s
}
Table-Driven Tests
tests := []struct {
name string
input string
want string
wantErr bool
}{
{"valid", "hello", "HELLO", false},
{"empty", "", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Process(tt.input)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
Go 1.25 Features
- testing/synctest: Deterministic concurrent testing with simulated clock
- encoding/json/v2: Experimental, 3-10x faster (GOEXPERIMENT=jsonv2)
- runtime/trace.FlightRecorder: Production trace capture on-demand
- Container-aware GOMAXPROCS: Auto-detects cgroup limits
- GreenTea GC: Experimental, lower latency (GOEXPERIMENT=greenteagc)
References
- PATTERNS.md - Detailed code patterns
- TESTING.md - Testing strategies with testify/mockery
- CLI.md - CLI application patterns
Tooling
go build ./... # Build
go test -race ./... # Test with race detector
golangci-lint run # Lint
mockery --all # Generate mocks
Gotchas
nil channel sends/receives block forever; closed channel receives return zero value immediately — select with a nil channel case disables that case, useful pattern but easy to do accidentally.
defer captures arguments at the call site, not at execution — defer fmt.Println(time.Now()) captures NOW, not the deferred time.
- Pre-Go 1.22 for-loop variable capture closures over ONE variable across all iterations — the goroutine-in-loop bug. Go 1.22 changed semantics; old habits create subtle bugs in mixed-version code.
errors.Is walks Unwrap() chains, BUT if a wrapped error implements Is(target error) bool itself, that custom Is wins over walking — confusing when migrating from xerrors.
sync.Pool items can be GC'd between Get and the next Put — never rely on a Pool to retain state.
1---
2name: writing-go
3description: Idiomatic Go 1.25+ development. Use when writing Go code, designing APIs, discussing Go patterns, or reviewing Go implementations. Emphasizes stdlib, concrete types, simple error handling, and minimal dependencies.
4allowed-tools: Read, Bash, Grep, Glob
5---
6
7# Go Development (1.25+)
8
9## Core Principles
10
11- **Stdlib first**: External deps only when justified
12- **Concrete types**: Define interfaces at consumer, return structs
13- **Composition**: Over inheritance, always
14- **Fail fast**: Clear errors with context
15- **Simple**: The obvious solution is usually correct
16
17## Quick Patterns
18
19### Error Handling
20
21```go
22if err := doThing(); err != nil {
23 return fmt.Errorf("do thing: %w", err)
24}
25```
26
27### Struct with Options
28
29```go
30type Server struct {
31 addr string
32 timeout time.Duration
33}
34
35func NewServer(addr string, opts ...Option) *Server {
36 s := &Server{addr: addr, timeout: 30 * time.Second}
37 for _, opt := range opts {
38 opt(s)
39 }
40 return s
41}
42```
43
44### Table-Driven Tests
45
46```go
47tests := []struct {
48 name string
49 input string
50 want string
51 wantErr bool
52}{
53 {"valid", "hello", "HELLO", false},
54 {"empty", "", "", true},
55}
56for _, tt := range tests {
57 t.Run(tt.name, func(t *testing.T) {
58 got, err := Process(tt.input)
59 if tt.wantErr {
60 require.Error(t, err)
61 return
62 }
63 require.NoError(t, err)
64 assert.Equal(t, tt.want, got)
65 })
66}
67```
68
69## Go 1.25 Features
70
71- **testing/synctest**: Deterministic concurrent testing with simulated clock
72- **encoding/json/v2**: Experimental, 3-10x faster (GOEXPERIMENT=jsonv2)
73- **runtime/trace.FlightRecorder**: Production trace capture on-demand
74- **Container-aware GOMAXPROCS**: Auto-detects cgroup limits
75- **GreenTea GC**: Experimental, lower latency (GOEXPERIMENT=greenteagc)
76
77## References
78
79- [PATTERNS.md](PATTERNS.md) - Detailed code patterns
80- [TESTING.md](TESTING.md) - Testing strategies with testify/mockery
81- [CLI.md](CLI.md) - CLI application patterns
82
83## Tooling
84
85```bash
86go build ./... # Build
87go test -race ./... # Test with race detector
88golangci-lint run # Lint
89mockery --all # Generate mocks
90```
91
92---
93
94## Gotchas
95
96- **`nil` channel sends/receives block forever; closed channel receives return zero value immediately** — `select` with a nil channel case disables that case, useful pattern but easy to do accidentally.
97- **`defer` captures arguments at the call site, not at execution** — `defer fmt.Println(time.Now())` captures NOW, not the deferred time.
98- **Pre-Go 1.22 for-loop variable capture closures over ONE variable across all iterations** — the goroutine-in-loop bug. Go 1.22 changed semantics; old habits create subtle bugs in mixed-version code.
99- **`errors.Is` walks `Unwrap()` chains, BUT if a wrapped error implements `Is(target error) bool` itself, that custom Is wins over walking** — confusing when migrating from xerrors.
100- **`sync.Pool` items can be GC'd between `Get` and the next `Put`** — never rely on a Pool to retain state.