Go Performance Review
Purpose
Identify performance issues and resource inefficiency in Go code. This skill exists because performance findings are almost all Medium severity and get systematically crowded out when mixed with High-severity Security/Concurrency findings.
Important distinction: lock contention is performance (here); race condition is concurrency (go-concurrency-review).
This skill does NOT cover: security, concurrency correctness, code style, test quality, error handling, or business logic — those belong to sibling vertical skills.
When To Use
- Code contains
make([]T, ...) or make(map[K]V, ...)
- Code builds strings in loops
- Code queries DB/Redis inside loops
- Code configures connection pools or HTTP clients
- Code runs on hot paths (per-request, high-frequency)
When NOT To Use
- Security vulnerabilities →
go-security-review
- Race conditions, goroutine leaks →
go-concurrency-review
- Code style/lint →
go-quality-review
- Error handling →
go-error-review
Mandatory Gates
1) Go Version Gate
Read go.mod. Key: strings.Clone (1.20+), slices.Clone (1.21+).
2) Anti-Example Suppression Gate
MUST quote specific evidence. Category match alone insufficient.
Embedded anti-examples:
- "Should pre-allocate slice" — when slice is small (<16 elements) or size is truly unknown at creation time. Must cite evidence the slice is large and size is known.
- "Should use sync.Pool" — when object is small and allocated infrequently (cold path, startup, config).
- "Struct fields should be reordered" — when struct has <4 fields or alignment savings <8 bytes.
- "Should use strings.Clone" — when both substring and parent are short-lived locals in same GC scope.
- "Add Count-First guard" — when the business domain guarantees the query always returns rows (e.g., seeded reference data, system-level lookups). Must cite evidence that total==0 is impossible in practice.
- Hot-path rule: Do NOT flag performance issues in cold-path code (startup, one-time init, config loading, test setup, CLI argument parsing). Must cite evidence of hot-path execution: "called per HTTP request", "inside for loop over N items", "in handler serving N QPS".
3) Generated Code Exclusion Gate
Exclude: *.pb.go, *_gen.go, mock_*.go.
Workflow
- Define scope — files/diff under review. Apply Generated Code Exclusion Gate.
- Gather evidence — read changed files, identify performance-relevant patterns:
make(), append(), loops, DB queries, strings.Builder, sync.Pool, HTTP clients.
- Load references — always load
go-performance-patterns.md; load go-database-patterns.md when DB code present.
- Classify hot-path vs cold-path — only flag issues in frequently executed code. Cite execution frequency evidence.
- Evaluate ALL 13 checklist items with quantification → suppress cold-path items → format output.
Grep-Gated Execution Protocol
This skill uses mechanical grep pre-scanning to guarantee zero missed checklist items. 11 of 13 items are grep-gated; 2 are semantic-only.
Execution Order
- Identify target files (from dispatch prompt, or write raw snippet to
$TMPDIR/review_snippet.go)
- Run grep for all grep-gated checklist items against target files
- HIT → run semantic analysis to confirm or reject (verify hot-path, quantify impact)
- MISS → auto-mark NOT FOUND, skip semantic analysis
- For compound patterns: run primary grep, then check secondary condition
- For semantic-only items (items 6, 9): full model reasoning
- Report only FOUND items
Grep Audit Line
Include in Execution Status: Grep pre-scan: X/11 items hit, Z confirmed as findings (2 semantic-only)
Key Compound Pattern: Slice Pre-allocation (Item 1)
This is the highest-priority compound pattern — it catches the most common performance miss:
- Primary:
make\(\[\] HIT
- Secondary: check if the make call has only 2 args (no capacity) AND an upper bound is known (e.g.,
len(input))
- Example:
make([]*User, 0) when len(userKeys) is available → FINDING
Performance Checklist (12 Items)
All Medium severity unless marked (Low).
| # |
Item |
Quantification Pattern |
Grep Pattern |
| 1 |
Missing slice pre-allocation |
make([]T, 0) when upper bound known → "N grow-and-copy → 1 allocation" |
make\(\[\] (compound: AND NOT 3-arg make with capacity — check for make\(\[\][^,]+,\s*\d+,\s*\d+\) absent) |
| 2 |
String concatenation in loop |
+= in loop → strings.Builder + Grow() — "N allocations + N copies → 1" |
\+=\s*""|\+=\s*\w+\s*$ (compound: inside for loop — semantic confirmation) |
| 3 |
N+1 query |
Individual DB/Redis calls in loop → batch WHERE IN or pipeline — "N round-trips → 1" |
\.Query|\.Exec|\.Get|\.Set (compound: inside for loop — N+1 detection) |
| 4 |
Missing connection pool config |
Missing SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime — connection exhaustion risk |
SetMaxOpenConns|SetMaxIdleConns|SetConnMaxLifetime|sql\.Open|pgx\.Connect |
| 5 |
Missing sync.Pool |
Hot-path allocations without pooling; pool without Reset before Put — quantify allocation frequency |
sync\.Pool |
| 6 |
Struct memory alignment |
Fields poorly ordered → fieldalignment tool — quantify savings in bytes per instance |
Semantic-Only (struct field alignment requires counting fields and sizes) |
| 7 |
Substring memory retention |
Large string sliced, small substring retains backing array → strings.Clone (Go 1.20+) |
strings\.Clone|[:]\w*\] (substring retention — semantic confirmation required) |
| 8 |
Oversized lock scope |
Mutex where atomic suffices; critical section includes non-critical I/O — quantify contention |
sync\.Mutex|sync\.RWMutex|\.Lock\(\) (compound: check if lock held across I/O) |
| 9 |
Missing sharded lock (Low) |
Single mutex on high-contention data → sharded locks — only for proven bottleneck |
Semantic-Only (sharded lock pattern requires understanding contention — rarely applicable) |
| 10 |
Missing buffered I/O |
Frequent small reads/writes without bufio — "5-50x syscall reduction" |
os\.Open|os\.Create|os\.Write|os\.Read|net\.Conn (compound: AND NOT bufio\.) |
| 11 |
Inefficient JSON encoding |
json.Marshal/Unmarshal on stream → json.NewEncoder/Decoder — "eliminates []byte allocation" |
json\.Marshal|json\.Unmarshal (compound: AND stream-compatible context — semantic) |
| 12 |
Untuned HTTP Transport |
http.DefaultClient without timeout; MaxIdleConnsPerHost default 2 too low for high-throughput |
http\.DefaultClient|http\.Get|http\.Post|http\.Client |
| 13 |
Missing Count-First guard in pagination query |
Function returns (list, total) with a Count + Find pair but no zero-guard. Reorder to Count-First: run Count → if total == 0 return early → only then run Find. Eliminates Find DB round-trip (full row-data transfer) for empty result sets — common for new tenants, inactive users, sparse data. Anti-example: when business domain guarantees total always > 0 (seeded reference tables, system lookups). |
\.Count\(& (compound: confirm .Find\( also present in same function AND no if.*total.*==.*0 or if total == 0 early-exit guard) |
Severity Rubric
Medium — Performance issue impacting latency, throughput, or resource usage under load.
Low — Minor optimization with limited real-world impact.
Evidence Rules
- Quantify when possible: "N+1 executes N round-trips", "slice grows through log2(N) allocations"
- For pre-allocation: state the known upper bound and current allocation
- Hot-path requirement: cite evidence code runs frequently (per-request, in loop, high-QPS handler)
- Do NOT flag cold-path optimizations
- Merge rule: same issue at ≥3 locations → one finding with location list
Output Format
Findings
[Medium|Low] Short Title
- ID: PERF-NNN
- Location:
path:line
- Impact: Quantified performance consequence (allocations, syscalls, round-trips)
- Evidence: Current code vs optimal pattern
- Recommendation: Specific fix with code example
- Action:
follow-up (performance issues are rarely must-fix)
Suppressed Items
[Suppressed] Short Title
- Reason: Anti-example matched + evidence cited (cold-path, small size, etc.)
Execution Status
Go version: X.Y
Grep pre-scan: X/11 items hit, Z confirmed as findings (2 semantic-only)
Excluded (generated): list or None
References loaded: list
Summary
1-2 lines. Count by severity.
Example Output
### Findings
#### [Medium] Slice Pre-allocation Missing in Hot Path
- **ID:** PERF-001
- **Location:** `internal/service/batch.go:42`
- **Impact:** len(userIDs) allocations per request instead of 1 — slice grows via append in loop processing user batch
- **Evidence:** `results := make([]User, 0)` at L42; `results = append(results, user)` in loop at L48; `len(userIDs)` is known at L40
- **Recommendation:** `results := make([]User, 0, len(userIDs))`
- **Action:** follow-up
#### [Medium] N+1 Query in Order Processing
- **ID:** PERF-002
- **Location:** `internal/service/order.go:55-60`
- **Impact:** N database round-trips per batch — one SELECT per order item
- **Evidence:** `for _, item := range items { product, _ := repo.GetProduct(ctx, item.ProductID) }` — N individual queries
- **Recommendation:** Batch: `products, err := repo.GetProductsByIDs(ctx, productIDs)` with `WHERE id IN (?)`
- **Action:** follow-up
### Suppressed Items
#### [Suppressed] Slice Pre-allocation in Config Loading
- **Reason:** `make([]Plugin, 0)` at config.go:12 — cold path (called once at startup, typically <5 plugins). Anti-example: "small slice in cold path"
### Execution Status
- Go version: 1.21
- Excluded (generated): None
- References loaded: go-performance-patterns.md, go-database-patterns.md
### Summary
2 Medium findings (slice pre-allocation, N+1 query). Cold-path items suppressed.
No-Finding Case
If no issues found: state No performance findings identified. Still output Execution Status.
Load References Selectively
| Reference |
Load When |
references/go-performance-patterns.md |
Always |
references/go-database-patterns.md |
Code involves database queries, connection pools |
references/go-review-anti-examples.md |
Always |
Review Discipline
- Performance only — not security, concurrency, quality, tests, errors, or logic
- Execute ALL 13 checklist items — this is the most commonly skipped dimension; isolation solves that
- Distinguish hot-path vs cold-path: only flag frequently executed code
- Quantify impact — "costs N allocations per request" is better than "suboptimal"
1---2name: go-performance-review3description: Review Go code for performance issues including slice/map pre-allocation, string concatenation, N+1 queries, connection pool configuration, sync.Pool, memory alignment, lock scope, buffered I/O, and HTTP transport tuning. Trigger when code contains make(), loops, database queries, string building, sync primitives, HTTP clients, or hot-path operations. Use for performance-focused review.4---56# Go Performance Review78## Purpose910Identify performance issues and resource inefficiency in Go code. This skill exists because performance findings are almost all Medium severity and get systematically crowded out when mixed with High-severity Security/Concurrency findings.1112Important distinction: **lock contention** is performance (here); **race condition** is concurrency (`go-concurrency-review`).1314This skill does NOT cover: security, concurrency correctness, code style, test quality, error handling, or business logic — those belong to sibling vertical skills.1516## When To Use17- Code contains `make([]T, ...)` or `make(map[K]V, ...)`18- Code builds strings in loops19- Code queries DB/Redis inside loops20- Code configures connection pools or HTTP clients21- Code runs on hot paths (per-request, high-frequency)2223## When NOT To Use24- Security vulnerabilities → `go-security-review`25- Race conditions, goroutine leaks → `go-concurrency-review`26- Code style/lint → `go-quality-review`27- Error handling → `go-error-review`2829## Mandatory Gates3031### 1) Go Version Gate32Read `go.mod`. Key: `strings.Clone` (1.20+), `slices.Clone` (1.21+).3334### 2) Anti-Example Suppression Gate35MUST quote specific evidence. Category match alone insufficient.3637Embedded anti-examples:38- **"Should pre-allocate slice"** — when slice is small (<16 elements) or size is truly unknown at creation time. Must cite evidence the slice is large and size is known.39- **"Should use sync.Pool"** — when object is small and allocated infrequently (cold path, startup, config).40- **"Struct fields should be reordered"** — when struct has <4 fields or alignment savings <8 bytes.41- **"Should use strings.Clone"** — when both substring and parent are short-lived locals in same GC scope.42- **"Add Count-First guard"** — when the business domain guarantees the query always returns rows (e.g., seeded reference data, system-level lookups). Must cite evidence that total==0 is impossible in practice.43- **Hot-path rule**: Do NOT flag performance issues in cold-path code (startup, one-time init, config loading, test setup, CLI argument parsing). Must cite evidence of hot-path execution: "called per HTTP request", "inside for loop over N items", "in handler serving N QPS".4445### 3) Generated Code Exclusion Gate46Exclude: `*.pb.go`, `*_gen.go`, `mock_*.go`.4748## Workflow49501. **Define scope** — files/diff under review. Apply Generated Code Exclusion Gate.512. **Gather evidence** — read changed files, identify performance-relevant patterns: `make()`, `append()`, loops, DB queries, `strings.Builder`, `sync.Pool`, HTTP clients.523. **Load references** — always load `go-performance-patterns.md`; load `go-database-patterns.md` when DB code present.534. **Classify hot-path vs cold-path** — only flag issues in frequently executed code. Cite execution frequency evidence.545. **Evaluate ALL 13 checklist items with quantification** → suppress cold-path items → format output.5556## Grep-Gated Execution Protocol5758This skill uses mechanical grep pre-scanning to guarantee zero missed checklist items. 11 of 13 items are grep-gated; 2 are semantic-only.5960### Execution Order611. Identify target files (from dispatch prompt, or write raw snippet to `$TMPDIR/review_snippet.go`)622. Run grep for all grep-gated checklist items against target files633. **HIT** → run semantic analysis to confirm or reject (verify hot-path, quantify impact)644. **MISS** → auto-mark NOT FOUND, skip semantic analysis655. For compound patterns: run primary grep, then check secondary condition666. For semantic-only items (items 6, 9): full model reasoning677. Report only FOUND items6869### Grep Audit Line70Include in Execution Status: `Grep pre-scan: X/11 items hit, Z confirmed as findings (2 semantic-only)`7172### Key Compound Pattern: Slice Pre-allocation (Item 1)73This is the highest-priority compound pattern — it catches the most common performance miss:74- Primary: `make\(\[\]` HIT75- Secondary: check if the make call has only 2 args (no capacity) AND an upper bound is known (e.g., `len(input)`)76- Example: `make([]*User, 0)` when `len(userKeys)` is available → FINDING7778## Performance Checklist (12 Items)7980All Medium severity unless marked (Low).8182| # | Item | Quantification Pattern | Grep Pattern |83|---|------|----------------------|-------------|84| 1 | **Missing slice pre-allocation** | `make([]T, 0)` when upper bound known → "N grow-and-copy → 1 allocation" | `make\(\[\]` (compound: AND NOT 3-arg make with capacity — check for `make\(\[\][^,]+,\s*\d+,\s*\d+\)` absent) |85| 2 | **String concatenation in loop** | `+=` in loop → `strings.Builder` + `Grow()` — "N allocations + N copies → 1" | `\+=\s*""\|\+=\s*\w+\s*$` (compound: inside `for` loop — semantic confirmation) |86| 3 | **N+1 query** | Individual DB/Redis calls in loop → batch `WHERE IN` or pipeline — "N round-trips → 1" | `\.Query\|\.Exec\|\.Get\|\.Set` (compound: inside `for` loop — N+1 detection) |87| 4 | **Missing connection pool config** | Missing `SetMaxOpenConns`, `SetMaxIdleConns`, `SetConnMaxLifetime` — connection exhaustion risk | `SetMaxOpenConns\|SetMaxIdleConns\|SetConnMaxLifetime\|sql\.Open\|pgx\.Connect` |88| 5 | **Missing sync.Pool** | Hot-path allocations without pooling; pool without Reset before Put — quantify allocation frequency | `sync\.Pool` |89| 6 | **Struct memory alignment** | Fields poorly ordered → `fieldalignment` tool — quantify savings in bytes per instance | Semantic-Only (struct field alignment requires counting fields and sizes) |90| 7 | **Substring memory retention** | Large string sliced, small substring retains backing array → `strings.Clone` (Go 1.20+) | `strings\.Clone\|[:]\w*\]` (substring retention — semantic confirmation required) |91| 8 | **Oversized lock scope** | Mutex where `atomic` suffices; critical section includes non-critical I/O — quantify contention | `sync\.Mutex\|sync\.RWMutex\|\.Lock\(\)` (compound: check if lock held across I/O) |92| 9 | **Missing sharded lock** (Low) | Single mutex on high-contention data → sharded locks — only for proven bottleneck | Semantic-Only (sharded lock pattern requires understanding contention — rarely applicable) |93| 10 | **Missing buffered I/O** | Frequent small reads/writes without `bufio` — "5-50x syscall reduction" | `os\.Open\|os\.Create\|os\.Write\|os\.Read\|net\.Conn` (compound: AND NOT `bufio\.`) |94| 11 | **Inefficient JSON encoding** | `json.Marshal`/`Unmarshal` on stream → `json.NewEncoder`/`Decoder` — "eliminates []byte allocation" | `json\.Marshal\|json\.Unmarshal` (compound: AND stream-compatible context — semantic) |95| 12 | **Untuned HTTP Transport** | `http.DefaultClient` without timeout; `MaxIdleConnsPerHost` default 2 too low for high-throughput | `http\.DefaultClient\|http\.Get\|http\.Post\|http\.Client` |96| 13 | **Missing Count-First guard in pagination query** | Function returns `(list, total)` with a `Count` + `Find` pair but no zero-guard. Reorder to Count-First: run `Count` → if `total == 0` return early → only then run `Find`. Eliminates `Find` DB round-trip (full row-data transfer) for empty result sets — common for new tenants, inactive users, sparse data. Anti-example: when business domain guarantees total always > 0 (seeded reference tables, system lookups). | `\.Count\(&` (compound: confirm `.Find\(` also present in same function AND no `if.*total.*==.*0` or `if total == 0` early-exit guard) |9798## Severity Rubric99100**Medium** — Performance issue impacting latency, throughput, or resource usage under load.101102**Low** — Minor optimization with limited real-world impact.103104## Evidence Rules105- **Quantify when possible**: "N+1 executes N round-trips", "slice grows through log2(N) allocations"106- For pre-allocation: state the known upper bound and current allocation107- **Hot-path requirement**: cite evidence code runs frequently (per-request, in loop, high-QPS handler)108- Do NOT flag cold-path optimizations109- **Merge rule**: same issue at ≥3 locations → one finding with location list110111## Output Format112113### Findings114#### [Medium|Low] Short Title115- **ID:** PERF-NNN116- **Location:** `path:line`117- **Impact:** Quantified performance consequence (allocations, syscalls, round-trips)118- **Evidence:** Current code vs optimal pattern119- **Recommendation:** Specific fix with code example120- **Action:** `follow-up` (performance issues are rarely `must-fix`)121122### Suppressed Items123#### [Suppressed] Short Title124- **Reason:** Anti-example matched + evidence cited (cold-path, small size, etc.)125126### Execution Status127- `Go version`: X.Y128- `Grep pre-scan`: X/11 items hit, Z confirmed as findings (2 semantic-only)129- `Excluded (generated)`: list or None130- `References loaded`: list131132### Summary1331-2 lines. Count by severity.134135## Example Output136137```138### Findings139140#### [Medium] Slice Pre-allocation Missing in Hot Path141- **ID:** PERF-001142- **Location:** `internal/service/batch.go:42`143- **Impact:** len(userIDs) allocations per request instead of 1 — slice grows via append in loop processing user batch144- **Evidence:** `results := make([]User, 0)` at L42; `results = append(results, user)` in loop at L48; `len(userIDs)` is known at L40145- **Recommendation:** `results := make([]User, 0, len(userIDs))`146- **Action:** follow-up147148#### [Medium] N+1 Query in Order Processing149- **ID:** PERF-002150- **Location:** `internal/service/order.go:55-60`151- **Impact:** N database round-trips per batch — one SELECT per order item152- **Evidence:** `for _, item := range items { product, _ := repo.GetProduct(ctx, item.ProductID) }` — N individual queries153- **Recommendation:** Batch: `products, err := repo.GetProductsByIDs(ctx, productIDs)` with `WHERE id IN (?)`154- **Action:** follow-up155156### Suppressed Items157#### [Suppressed] Slice Pre-allocation in Config Loading158- **Reason:** `make([]Plugin, 0)` at config.go:12 — cold path (called once at startup, typically <5 plugins). Anti-example: "small slice in cold path"159160### Execution Status161- Go version: 1.21162- Excluded (generated): None163- References loaded: go-performance-patterns.md, go-database-patterns.md164165### Summary1662 Medium findings (slice pre-allocation, N+1 query). Cold-path items suppressed.167```168169## No-Finding Case170If no issues found: state `No performance findings identified.` Still output Execution Status.171172## Load References Selectively173174| Reference | Load When |175|-----------|-----------|176| `references/go-performance-patterns.md` | Always |177| `references/go-database-patterns.md` | Code involves database queries, connection pools |178| `references/go-review-anti-examples.md` | Always |179180## Review Discipline181- **Performance only** — not security, concurrency, quality, tests, errors, or logic182- **Execute ALL 13 checklist items** — this is the most commonly skipped dimension; isolation solves that183- **Distinguish hot-path vs cold-path**: only flag frequently executed code184- **Quantify impact** — "costs N allocations per request" is better than "suboptimal"