
Golang Troubleshooting
- 34.4k installs
- 2.9k repo stars
- Updated August 1, 2026
- samber/cc-skills-golang
golang-troubleshooting is a Claude Code skill that enforces a disciplined, test-first process for debugging Go bugs, crashes, deadlocks, and races.
About
golang-troubleshooting is an agent skill that forces coding agents into a disciplined, test-first process when debugging Go code. It routes a symptom (compile failure, wrong output, panic, race, hang, high CPU, memory growth, latency) through a decision tree and Golden Rules that require reproducing before fixing and finding the root cause. It escalates tooling incrementally from fmt.Println and test isolation to pprof, Delve, and GODEBUG. A developer uses it when a Go service crashes, deadlocks, or misbehaves and needs systematic diagnosis instead of guessed fixes.
- Test-first, root-cause-first Go debugging methodology
- Symptom decision tree: panics, races, hangs, CPU, memory, latency
- Incremental tooling from prints to pprof, Delve, and GODEBUG
Golang Troubleshooting by the numbers
- 34,356 all-time installs (skills.sh)
- +386 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #10 of 596 Debugging skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
golang-troubleshooting capabilities & compatibility
Free, no API key; requires the go toolchain and dlv (Delve)
- Capabilities
- go debugging · root cause analysis · race detection · profiling · test driven debugging
- Use cases
- debugging · testing
- Pricing
- Free
What golang-troubleshooting says it does
**NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST.** Symptom fixes create new bugs and waste time.
**Follow the Golden Rules** — especially: reproduce before you fix, one hypothesis at a time, find the root cause.
**Never propose a fix you cannot explain.** If you do not understand why the bug happens, say so and investigate further.
npx skills add https://github.com/samber/cc-skills-golang --skill golang-troubleshootingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 34.4k |
|---|---|
| repo stars | ★ 2.9k |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 1, 2026 |
| Repository | samber/cc-skills-golang ↗ |
What it does
debugging
Who is it for?
Go developers debugging a specific bug, crash, race, hang, or performance problem who want root-cause analysis over symptom fixes.
Skip if: Interpreting profiles/benchmarking (golang-benchmark) or applying optimization patterns (golang-performance).
When should I use this skill?
When encountering bugs, crashes, deadlocks, or unexpected behavior in Go code - the 'something is wrong' situation.
What you get
A reproduced failing test and an explained root-cause fix rather than a guessed symptom patch.
- A failing test that reproduces the bug
- An explained root-cause fix
By the numbers
- 11 reference guides (methodology, common-go-bugs, concurrency, pprof, production-debug, etc.)
- Decision tree covering 9 symptom categories
Files
Persona: You are a Go systems debugger. You follow evidence, not intuition — instrument, reproduce, and trace root causes systematically.
Thinking mode: Use ultrathink for debugging and root cause analysis. Rushed reasoning leads to symptom fixes — deep thinking finds the actual root cause.
Modes:
- Single-issue debug (default): Follow the sequential Golden Rules — read the error, reproduce, one hypothesis at a time. Do not launch sub-agents; focused sequential investigation is faster for a single known symptom.
- Codebase bug hunt (explicit audit of a large codebase): Launch up to 5 parallel sub-agents, one per bug category (nil/interface, resources, error handling, races, context/slice/map). Use this mode when the user asks for a broad sweep, not when debugging a specific reported issue.
Dependencies:
- dlv:
go install github.com/go-delve/delve/cmd/dlv@latest
Go Troubleshooting Guide
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST. Symptom fixes create new bugs and waste time. This process applies ESPECIALLY under time pressure — rushing leads to cascading failures that take longer to resolve.
When the user reports a bug, crash, performance problem, or unexpected behavior in Go code:
1. Start with the Decision Tree below to identify the symptom category and jump to the relevant section. 2. Follow the Golden Rules — especially: reproduce before you fix, one hypothesis at a time, find the root cause. 3. Work through the General Debugging Methodology step by step. Do not skip steps. 4. Watch for Red Flags in your own reasoning. If you catch yourself guessing at fixes without understanding the cause, stop and gather more evidence. 5. Escalate tools incrementally. Start with the simplest diagnostic (fmt.Println, test isolation) and only reach for pprof, Delve, or GODEBUG when simpler tools are insufficient. 6. Never propose a fix you cannot explain. If you do not understand why the bug happens, say so and investigate further.
Quick Decision Tree
WHAT ARE YOU SEEING?
"Build won't compile"
→ go build ./... 2>&1, go vet ./...
→ See [compilation.md](./references/compilation.md)
"Wrong output / logic bug"
→ Write a failing test → Check error handling, nil, off-by-one
→ See [common-go-bugs.md](./references/common-go-bugs.md), [testing-debug.md](./references/testing-debug.md)
"Random crashes / panics"
→ GOTRACEBACK=all ./app → go test -race ./...
→ See [common-go-bugs.md](./references/common-go-bugs.md), [diagnostic-tools.md](./references/diagnostic-tools.md)
"Sometimes works, sometimes fails"
→ go test -race ./...
→ See [concurrency-debug.md](./references/concurrency-debug.md), [testing-debug.md](./references/testing-debug.md)
"Program hangs / frozen"
→ curl localhost:6060/debug/pprof/goroutine?debug=2
→ See [concurrency-debug.md](./references/concurrency-debug.md), [pprof.md](./references/pprof.md)
"High CPU usage"
→ pprof CPU profiling
→ See [performance-debug.md](./references/performance-debug.md), [pprof.md](./references/pprof.md)
"Memory growing over time"
→ pprof heap profiling
→ See [performance-debug.md](./references/performance-debug.md), [concurrency-debug.md](./references/concurrency-debug.md)
"Slow / high latency / p99 spikes"
→ CPU + mutex + block profiles
→ See [performance-debug.md](./references/performance-debug.md), [diagnostic-tools.md](./references/diagnostic-tools.md)
"Simple bug, easy to reproduce"
→ Write a test, add fmt.Println / log.Debug
→ See [testing-debug.md](./references/testing-debug.md)Remember: Read the Error → Reproduce → Measure One Thing → Fix → Verify
Most Go bugs are: missing error checks, nil pointers, forgotten context cancel, unclosed resources, race conditions, or silent error swallowing.
The Golden Rules
1. Read the Error Message First
Go error messages are precise. Read them fully before doing anything else:
- File and line number → go directly there
- Type mismatch → check function signatures, interface satisfaction
- "undefined" → check imports, exported names, build tags
- "cannot use X as Y" → check concrete types vs interfaces
2. Reproduce Before You Fix
NEVER debug by guessing — reproduce first. Always:
- Write a failing test that captures the bug
- Make it deterministic
- Isolate the minimal failing example
- Use
git bisectto find the breaking commit
3. If You Don't Measure It, You're Guessing
Never rely on intuition for performance or concurrency bugs:
- pprof over intuition
- race detector over reasoning
- benchmarks over assumptions
4. One Hypothesis at a Time
Change one thing, measure, confirm. If you change three things at once, you learn nothing.
5. Find the Root Cause — No Workarounds
A band-aid fix that masks the symptom IS NOT ACCEPTABLE. You MUST understand why the bug happens before writing a fix.
When you don't understand the issue:
- Trace the data flow backwards from the symptom to its origin.
- Question your assumptions. The code you trust might be wrong.
- Ask "why" five times. Keep going until you reach the actual root cause.
- Perform more troubleshooting checks. More fmt.Println, more output inspection...
6. Research the Codebase, Not Just the Diff
Before flagging a bug or proposing a fix, trace the data flow and check for upstream handling. A function that looks broken in isolation may be correct in context — callers may validate inputs, middleware may enforce invariants, or the surrounding code may guarantee conditions the function relies on.
1. Trace callers — who calls this function and with what values? Call sites can be found with code search tools. 2. Check upstream validation — input parsing, type conversions, or guard clauses earlier in the chain may make the "bug" unreachable. 3. Read the surrounding code — middleware, interceptors, or init functions may set up state the function depends on.
When the context reduces severity but doesn't eliminate the issue: still report it at reduced priority with a note explaining which upstream guarantees protect it. Add a brief inline comment (e.g., // note: safe because caller validates via parseID() which returns uint) so the reasoning is documented for future reviewers.
7. Start Simple
Sometimes fmt.Println IS the right tool for local debugging. Escalate tools only when simpler approaches fail. NEVER use fmt.Println for production debugging — use slog.
Red Flags: You're Debugging Wrong
If any of these are happening, stop and return to Step 1:
- "Quick fix for now, investigate later" — There is no "later". Find the root cause.
- Multiple simultaneous changes — One hypothesis at a time.
- Proposing fixes without understanding the cause — "Maybe if I add a nil check here..." is guessing, not debugging.
- Each fix reveals a new problem — You're treating symptoms. The real bug is elsewhere.
- 3+ fix attempts on the same issue — You have the wrong mental model. Re-read the code, trace the data flow from scratch.
- "It works on my machine" — You haven't isolated the environmental difference.
- Blaming the framework/stdlib/compiler — It's almost never a Go bug. Verify your code first.
Reference Files
- [General Debugging Methodology](./references/methodology.md) — The systematic 10-step process: define symptoms, isolate reproduction, form one hypothesis, test it, verify the root cause, and defend against regressions. Escalation guide: when to escalate from
fmt.Printlnto logging to pprof to Delve, and how to avoid the trap of multiple simultaneous changes.
- [Common Go Bugs](./references/common-go-bugs.md) — The bugs that crash Go code: nil pointer dereferences, interface nil gotcha (typed nil ≠ nil), variable shadowing, slice/map/defer/error/context pitfalls, race conditions, JSON unmarshaling surprises, unclosed resources. Each with reproduction patterns and fixes.
- [Test-Driven Debugging](./references/testing-debug.md) — Why writing a failing test is the first step of debugging. Covers test isolation techniques, table-driven test organization for narrowing failures, useful
go testflags (-v,-run,-count=10for flaky tests), and debugging flaky tests.
- [Concurrency Debugging](./references/concurrency-debug.md) — Race conditions, deadlocks, goroutine leaks. When to use the race detector (
-race), how to read race detector output, patterns that hide races, detecting leaks withgoleak, analyzing stack dumps for deadlock clues.
- [Performance Troubleshooting](./references/performance-debug.md) — When your code is slow: CPU profiling workflow, memory analysis (heap vs alloc_objects profiles, finding leaks), lock contention (mutex profile), and I/O blocking (goroutine profile). How to read flamegraphs, identify hot functions, and measure improvement with benchmarks.
- [pprof Reference](./references/pprof.md) — Complete pprof manual. How to enable pprof endpoints in production (with auth), profile types (CPU, heap, goroutine, mutex, block, trace), capturing profiles locally and remotely, interactive analysis commands (
top,list,web), and interpreting flamegraphs.
- [Diagnostic Tools](./references/diagnostic-tools.md) — Auxiliary tools for specific symptoms. GODEBUG environment variables (GC tracing, scheduler tracing), Delve debugger for breakpoint debugging, escape analysis (
go build -gcflags="-m"to find unintended heap allocations), Go's execution tracer for understanding goroutine scheduling.
- [Production Debugging](./references/production-debug.md) — Debugging live production systems without stopping them. Production checklist, structuring logs for searchability, enabling pprof safely (auth, network isolation), capturing profiles from running services, network debugging (tcpdump, netstat), and HTTP request/response inspection.
- [Compilation Issues](./references/compilation.md) — Build failures: module version conflicts, CGO linking problems, version mismatch between
go.modand installed Go version, platform-specific build tags preventing cross-compilation.
- [Code Review Red Flags](./references/code-review-flags.md) — Patterns to watch during code review that signal potential bugs: unchecked errors, missing nil checks, concurrent map access, goroutines without clear exit, resource leaks from defer in loops.
Cross-References
- → See
samber/cc-skills-golang@golang-performanceskill for optimization patterns after identifying bottlenecks - → See
samber/cc-skills-golang@golang-observabilityskill for metrics, alerting, and Grafana dashboards for Go runtime monitoring - → See
samber/cc-skills@promql-cliskill for querying Prometheus metrics during production incident investigation - → See
samber/cc-skills-golang@golang-concurrency,samber/cc-skills-golang@golang-safety,samber/cc-skills-golang@golang-error-handlingskills
[
{
"id": 1,
"name": "reproduce-before-fix-failing-test-first",
"description": "Tests Golden Rule #2: model must write a failing test before proposing any fix, not jump to code changes",
"prompt": "My Go HTTP handler returns 500 for some requests but I can't figure out which ones. The handler parses a JSON body with `json.NewDecoder(r.Body).Decode(&req)` and then calls `processOrder(req)`. Some users report getting 500 errors. Can you fix this?",
"trap": "Model jumps directly to adding nil checks or error handling in the handler code without first writing a test that reproduces the failure",
"assertions": [
{"id": "1.1", "text": "Writes a failing test (or proposes writing one) BEFORE suggesting any code fix"},
{"id": "1.2", "text": "Test covers multiple input variations to identify which inputs trigger the 500"},
{"id": "1.3", "text": "Does NOT propose a fix without first understanding/reproducing the root cause"},
{"id": "1.4", "text": "Asks clarifying questions about the error (logs, request bodies, frequency) or proposes to gather evidence"},
{"id": "1.5", "text": "Mentions running the test to confirm it reproduces the issue before fixing"}
]
},
{
"id": 2,
"name": "one-hypothesis-at-a-time",
"description": "Tests Golden Rule #4: change one thing and measure, not multiple simultaneous changes",
"prompt": "My Go web service is slow and sometimes returns errors. I think it might be the database queries, or maybe the JSON marshaling, or possibly goroutine contention. Can you fix all three at once to save time? Add connection pooling, switch to a faster JSON library, and add mutexes around shared state.",
"trap": "Model applies all three fixes simultaneously as requested, violating the one-hypothesis-at-a-time rule",
"assertions": [
{"id": "2.1", "text": "Refuses or strongly advises against making all three changes simultaneously"},
{"id": "2.2", "text": "Recommends measuring/profiling first to identify which problem is actually causing the symptoms"},
{"id": "2.3", "text": "Suggests testing one hypothesis at a time with measurement between changes"},
{"id": "2.4", "text": "Mentions pprof, benchmarks, or race detector as diagnostic tools to identify the real bottleneck"},
{"id": "2.5", "text": "Explains why multiple simultaneous changes are harmful (can't tell what worked, may introduce new bugs)"}
]
},
{
"id": 3,
"name": "root-cause-not-symptom-fix",
"description": "Tests Golden Rule #5 and Step 8: fix at the source where bad data originates, not where the panic occurs",
"prompt": "My Go HTTP server panics with `nil pointer dereference` in the handler when accessing `s.db.Query(...)`. I added a nil check `if s.db == nil { return }` but now the handler silently returns empty responses. How do I fix this properly?",
"trap": "Model suggests improving the nil check (better error message, logging) rather than tracing to the root cause — why is s.db nil in the first place",
"assertions": [
{"id": "3.1", "text": "Identifies that the nil check in the handler is a symptom fix, not the root cause"},
{"id": "3.2", "text": "Traces backward to the constructor/initialization code to find why db is nil"},
{"id": "3.3", "text": "Suggests validating db != nil in the constructor (e.g., NewServer) and failing fast there"},
{"id": "3.4", "text": "Does NOT suggest improving the nil check in the handler as the primary fix"},
{"id": "3.5", "text": "Explains that fixing at the symptom location masks the real bug"}
]
},
{
"id": 4,
"name": "interface-nil-gotcha",
"description": "Tests the interface nil gotcha from common-go-bugs: typed nil in interface is not nil",
"prompt": "I have this Go code and the error branch always executes even though no error occurred. Debug this:\n\n```go\ntype ValidationError struct{ Field string }\nfunc (e *ValidationError) Error() string { return e.Field + \" invalid\" }\n\nfunc validate(s string) error {\n var verr *ValidationError\n if s == \"\" {\n verr = &ValidationError{Field: \"name\"}\n }\n return verr\n}\n\nfunc main() {\n if err := validate(\"hello\"); err != nil {\n fmt.Println(\"error:\", err) // This always prints!\n }\n}\n```",
"trap": "Model suggests adding a nil check on verr before returning, or restructuring the if/else, without explaining the interface nil gotcha",
"assertions": [
{"id": "4.1", "text": "Identifies the interface nil gotcha: a typed nil *ValidationError wrapped in an error interface is NOT a nil interface"},
{"id": "4.2", "text": "Explains that the interface has a non-nil type descriptor even when the pointer value is nil"},
{"id": "4.3", "text": "Recommends returning nil explicitly (return nil) instead of returning the typed nil variable"},
{"id": "4.4", "text": "Does NOT suggest adding an if verr == nil check before return as the primary fix — the correct fix is to return nil explicitly when there's no error"},
{"id": "4.5", "text": "Shows or describes the correct fix: check verr != nil before return, and return nil in the else branch"}
]
},
{
"id": 5,
"name": "variable-shadowing-err",
"description": "Tests the variable shadowing with := from common-go-bugs: inner err shadows outer err",
"prompt": "My Go function always returns nil error even when someFunc() fails. I verified that someFunc() does return errors in certain cases. What's wrong?\n\n```go\nfunc processData() error {\n var err error\n if needsProcessing {\n result, err := someFunc()\n if err != nil {\n return err\n }\n use(result)\n }\n return err\n}\n```",
"trap": "Model focuses on the early return path working correctly and misses that the outer err is always nil because := created a new variable",
"assertions": [
{"id": "5.1", "text": "Identifies that := inside the if block creates a NEW err variable that shadows the outer one"},
{"id": "5.2", "text": "Explains that the outer err remains nil because the inner := never assigned to it"},
{"id": "5.3", "text": "Recommends using = (assignment) instead of := to assign to the outer err variable"},
{"id": "5.4", "text": "Shows the fix: declare result separately (var result ResultType) and use result, err = someFunc()"},
{"id": "5.5", "text": "Mentions the golang.org/x/tools shadow analyzer as a detection tool, without relying on the obsolete go vet shadow flag"}
]
},
{
"id": 6,
"name": "defer-in-loop-resource-leak",
"description": "Tests the defer-in-loop gotcha from common-go-bugs: deferred calls pile up until function returns",
"prompt": "My Go program runs out of file descriptors when processing a large directory of files. Here's the code:\n\n```go\nfunc processFiles(paths []string) error {\n for _, p := range paths {\n f, err := os.Open(p)\n if err != nil {\n return err\n }\n defer f.Close()\n data, err := io.ReadAll(f)\n if err != nil {\n return err\n }\n process(data)\n }\n return nil\n}\n```\nIt works for small directories but crashes with 'too many open files' for large ones.",
"trap": "Model suggests increasing the file descriptor limit (ulimit) instead of fixing the defer-in-loop bug",
"assertions": [
{"id": "6.1", "text": "Identifies that defer f.Close() inside a for loop keeps all files open until the function returns"},
{"id": "6.2", "text": "Recommends wrapping the loop body in an anonymous function (closure) so defer runs each iteration"},
{"id": "6.3", "text": "Does NOT suggest increasing ulimit or file descriptor limits as the primary solution"},
{"id": "6.4", "text": "Shows the correct pattern with func() { f, err := os.Open(...); defer f.Close(); ... }()"},
{"id": "6.5", "text": "Alternatively suggests extracting the loop body into a named function"}
]
},
{
"id": 7,
"name": "break-in-select-inside-for-loop",
"description": "Tests the break-in-select gotcha: bare break only exits select, not the enclosing for loop",
"prompt": "My Go program's message consumer loop never terminates. When I send 'quit' on the channel, the loop keeps running. What's wrong?\n\n```go\nfor {\n select {\n case msg := <-ch:\n if msg == \"quit\" {\n log.Println(\"shutting down\")\n break\n }\n handleMessage(msg)\n case <-ctx.Done():\n break\n }\n}\n```",
"trap": "Model suggests the channel isn't receiving 'quit' or there's a timing issue, rather than identifying the break-in-select gotcha",
"assertions": [
{"id": "7.1", "text": "Identifies that break inside a select only exits the select statement, not the for loop"},
{"id": "7.2", "text": "Recommends using a labeled break (e.g., break loop) with a label on the for statement"},
{"id": "7.3", "text": "Shows the correct pattern with a label like 'loop:' on the for statement and 'break loop' inside select"},
{"id": "7.4", "text": "Also fixes the ctx.Done() case which has the same break issue"},
{"id": "7.5", "text": "Alternatively mentions return as a solution if the function should exit entirely"}
]
},
{
"id": 8,
"name": "concurrent-map-fatal-not-panic",
"description": "Tests knowledge that concurrent map access is a fatal error that cannot be recovered, unlike most panics",
"prompt": "My Go web server occasionally crashes despite having a recover() middleware that catches panics. The error message says 'concurrent map read and map write'. I thought recover() catches all panics — why isn't it working? How should I protect against this?",
"trap": "Model suggests the recover middleware is misconfigured or needs to be higher in the middleware chain, rather than explaining that concurrent map access is fatal and unrecoverable",
"assertions": [
{"id": "8.1", "text": "Explains that concurrent map read/write is a FATAL error that cannot be caught by recover()"},
{"id": "8.2", "text": "Distinguishes this from regular panics — the Go runtime kills the process immediately"},
{"id": "8.3", "text": "Recommends protecting the map with sync.RWMutex or using sync.Map"},
{"id": "8.4", "text": "Recommends using go test -race to find the race condition"},
{"id": "8.5", "text": "Does NOT suggest fixing the recover middleware as the solution"}
]
},
{
"id": 9,
"name": "waitgroup-add-inside-goroutine",
"description": "Tests the WaitGroup.Add placement gotcha: Add inside goroutine races with Wait",
"prompt": "My Go test passes most of the time but occasionally fails with 'expected 10 results, got 7' (or some other number less than 10). The code launches goroutines to process items in parallel:\n\n```go\nvar wg sync.WaitGroup\nresults := make([]int, 0, 10)\nvar mu sync.Mutex\nfor i := 0; i < 10; i++ {\n go func(n int) {\n wg.Add(1)\n defer wg.Done()\n val := process(n)\n mu.Lock()\n results = append(results, val)\n mu.Unlock()\n }(i)\n}\nwg.Wait()\n```",
"trap": "Model focuses on the mutex/slice synchronization and misses that wg.Add(1) is inside the goroutine, racing with wg.Wait()",
"assertions": [
{"id": "9.1", "text": "Identifies that wg.Add(1) is called inside the goroutine instead of before it"},
{"id": "9.2", "text": "Explains that wg.Wait() may return before all goroutines have called wg.Add(1)"},
{"id": "9.3", "text": "Recommends moving wg.Add(1) before the go func() call"},
{"id": "9.4", "text": "Notes this is a race condition that passes most of the time but fails intermittently"},
{"id": "9.5", "text": "Does NOT focus primarily on the mutex/slice synchronization as the root cause"}
]
},
{
"id": 10,
"name": "missing-return-after-http-error",
"description": "Tests the missing return after http.Error() bug from common-go-bugs",
"prompt": "My Go API endpoint has a security vulnerability — unauthorized users can sometimes access protected resources. The handler checks authorization and sends a 403 Forbidden response, but the protected action still executes. Here's the code:\n\n```go\nfunc handleDelete(w http.ResponseWriter, r *http.Request) {\n if !isAuthorized(r) {\n http.Error(w, \"Forbidden\", http.StatusForbidden)\n }\n if err := deleteResource(r.Context(), r.URL.Query().Get(\"id\")); err != nil {\n http.Error(w, err.Error(), 500)\n }\n w.WriteHeader(http.StatusNoContent)\n}\n```",
"trap": "Model suggests the isAuthorized function is buggy rather than noticing the missing return after http.Error()",
"assertions": [
{"id": "10.1", "text": "Identifies the missing return statement after http.Error(w, 'Forbidden', http.StatusForbidden)"},
{"id": "10.2", "text": "Explains that http.Error() does NOT stop handler execution — it only writes to the ResponseWriter"},
{"id": "10.3", "text": "Adds return statements after each http.Error() call"},
{"id": "10.4", "text": "Does NOT primarily blame the isAuthorized function"},
{"id": "10.5", "text": "Mentions this is a common Go bug pattern and a security concern"}
]
},
{
"id": 11,
"name": "json-numbers-float64-interface",
"description": "Tests JSON unmarshaling gotcha: numbers into interface{} become float64, not int",
"prompt": "My Go code panics when processing JSON API responses. The JSON looks like `{\"user_id\": 1234567890123456789}`. I unmarshal into `map[string]interface{}` and then type-assert the user_id to int64:\n\n```go\nvar result map[string]interface{}\njson.Unmarshal(data, &result)\nuserID := result[\"user_id\"].(int64)\n```\nWhy does this panic?",
"trap": "Model suggests the JSON is malformed or the field name doesn't match, rather than explaining the float64 type gotcha",
"assertions": [
{"id": "11.1", "text": "Explains that JSON numbers unmarshaled into interface{} become float64, not int or int64"},
{"id": "11.2", "text": "Notes that large integers (> 2^53) silently lose precision when stored as float64"},
{"id": "11.3", "text": "Recommends using a typed struct with int64 field as the preferred solution"},
{"id": "11.4", "text": "Alternatively mentions json.NewDecoder with UseNumber() and json.Number for when interface{} is required"},
{"id": "11.5", "text": "Explains the type assertion panics because the actual type is float64, not int64"}
]
},
{
"id": 12,
"name": "strings-trim-vs-trimprefix",
"description": "Tests the strings.Trim character-set gotcha from common-go-bugs",
"prompt": "My Go function strips the 'application/' prefix from MIME types but gives wrong results for some types. `strings.Trim(\"application/json\", \"application/\")` returns 'js' instead of 'json'. Is this a Go bug?",
"trap": "Model suggests it's a Go bug or suggests a regex-based workaround instead of explaining Trim treats the second argument as a character set",
"assertions": [
{"id": "12.1", "text": "Explains that strings.Trim treats its second argument as a SET of characters to strip, not as a substring"},
{"id": "12.2", "text": "Shows why 'json' becomes 'js' — the characters j, o, n are in the set {a,p,l,i,c,t,o,n,/}"},
{"id": "12.3", "text": "Recommends strings.TrimPrefix for removing a substring prefix"},
{"id": "12.4", "text": "Mentions strings.TrimSuffix for removing suffixes"},
{"id": "12.5", "text": "Confirms this is NOT a Go bug — it's working as documented"}
]
},
{
"id": 13,
"name": "closed-channel-busy-loop-in-select",
"description": "Tests the closed channel in select causing busy loop from common-go-bugs",
"prompt": "After running for a few hours, my Go worker's CPU usage jumps to 100% even when there's no work. The worker reads from a channel in a select. Sometimes the upstream producer closes the channel when it's done. Here's the worker:\n\n```go\nfunc worker(ch <-chan Job, done <-chan struct{}) {\n for {\n select {\n case job := <-ch:\n process(job)\n case <-done:\n return\n }\n }\n}\n```",
"trap": "Model suggests adding a time.Sleep in a default case or reducing GOMAXPROCS, rather than identifying that a closed channel fires continuously in select",
"assertions": [
{"id": "13.1", "text": "Identifies that a closed channel always returns immediately (zero value) in a select case"},
{"id": "13.2", "text": "Explains this causes the select case to fire continuously — a busy loop burning CPU"},
{"id": "13.3", "text": "Recommends using the comma-ok idiom (job, ok := <-ch) and nil-ing the channel when closed (ch = nil)"},
{"id": "13.4", "text": "Explains that a nil channel blocks forever in select, effectively disabling that case"},
{"id": "13.5", "text": "Does NOT suggest adding a default case with time.Sleep as the fix"}
]
},
{
"id": 14,
"name": "select-default-spin-loop",
"description": "Tests the select-with-default busy-wait pattern from common-go-bugs",
"prompt": "I need non-blocking channel reads in my Go message processor. I have a for loop with a select that checks a channel and a default case. The code works but uses 100% CPU even when idle. How do I fix this without blocking?\n\n```go\nfor {\n select {\n case msg := <-incoming:\n handleMsg(msg)\n default:\n // check other conditions\n if shouldStop() {\n return\n }\n }\n}\n```",
"trap": "Model keeps the default case and just adds more logic to it, rather than restructuring to remove the busy-wait",
"assertions": [
{"id": "14.1", "text": "Identifies that select with default inside a for loop is a busy-wait spin loop"},
{"id": "14.2", "text": "Explains that default runs immediately when no channel is ready, creating a tight loop"},
{"id": "14.3", "text": "Recommends removing the default case and using a second channel or context for the stop signal"},
{"id": "14.4", "text": "Alternatively suggests adding a time.Sleep or ticker in the default to yield CPU if non-blocking is truly required"},
{"id": "14.5", "text": "Shows a solution using a ctx.Done() or stop channel in a second select case"}
]
},
{
"id": 15,
"name": "enum-zero-value-iota-ambiguity",
"description": "Tests the iota zero value ambiguity from common-go-bugs",
"prompt": "I have a Go enum for user roles using iota. Some users are getting Admin privileges by default when they register, even though I didn't set their role. The zero value of Role seems to be Admin. How should I fix this?\n\n```go\ntype Role int\nconst (\n Admin Role = iota // 0\n Editor // 1\n Viewer // 2\n)\n\ntype User struct {\n Name string\n Role Role\n}\n```",
"trap": "Model suggests setting new users' Role field to Viewer explicitly in the constructor, rather than fixing the enum design",
"assertions": [
{"id": "15.1", "text": "Identifies that iota starting at 0 makes the zero value (default for uninitialized fields) equal to Admin"},
{"id": "15.2", "text": "Recommends reserving 0 for an Unknown/Unspecified sentinel value"},
{"id": "15.3", "text": "Shows the pattern: RoleUnknown Role = iota, then Admin, Editor, Viewer"},
{"id": "15.4", "text": "Explains this applies to any enum — zero value should be the 'unset' state, not a valid value"},
{"id": "15.5", "text": "Does NOT primarily suggest fixing it in the constructor or registration logic"}
]
},
{
"id": 16,
"name": "recover-only-same-goroutine",
"description": "Tests the recover() goroutine boundary from common-go-bugs",
"prompt": "My Go server has a panic recovery middleware but child goroutines still crash the entire process. I have:\n\n```go\nfunc handler(w http.ResponseWriter, r *http.Request) {\n defer func() {\n if r := recover(); r != nil {\n http.Error(w, \"Internal Error\", 500)\n }\n }()\n go processAsync(r.Context(), extractData(r))\n w.WriteHeader(http.StatusAccepted)\n}\n```\nWhen processAsync panics, the whole server crashes instead of just returning 500.",
"trap": "Model suggests wrapping the recover middleware differently or using a global recover, not understanding that recover only works within the same goroutine",
"assertions": [
{"id": "16.1", "text": "Explains that recover() can ONLY catch panics in the same goroutine where it is deferred"},
{"id": "16.2", "text": "States that a panic in a child goroutine will crash the entire program regardless of parent recovery"},
{"id": "16.3", "text": "Recommends adding defer/recover inside the child goroutine (processAsync or its wrapper)"},
{"id": "16.4", "text": "Shows the pattern: go func() { defer func() { if r := recover()... }(); processAsync(...) }()"},
{"id": "16.5", "text": "Does NOT suggest reconfiguring the parent middleware as the solution"}
]
},
{
"id": 17,
"name": "os-exit-skips-defers",
"description": "Tests os.Exit / log.Fatal skipping deferred functions from common-go-bugs",
"prompt": "My Go CLI tool creates temp files and defers their cleanup, but sometimes temp files are left behind. The code structure is:\n\n```go\nfunc main() {\n tmpFile, _ := os.CreateTemp(\"\", \"data-*\")\n defer os.Remove(tmpFile.Name())\n defer tmpFile.Close()\n\n if err := processData(tmpFile); err != nil {\n log.Fatalf(\"processing failed: %v\", err)\n }\n // ... use results\n}\n```",
"trap": "Model suggests the error path doesn't clean up properly but misses that log.Fatal calls os.Exit which skips ALL deferred functions",
"assertions": [
{"id": "17.1", "text": "Identifies that log.Fatal (or log.Fatalf) calls os.Exit(1) internally"},
{"id": "17.2", "text": "Explains that os.Exit skips all deferred functions — cleanup never runs"},
{"id": "17.3", "text": "Recommends restructuring to avoid log.Fatal — use a run() function pattern or return errors"},
{"id": "17.4", "text": "Shows the pattern: move logic into a run() error function, call os.Exit in main only after run returns"},
{"id": "17.5", "text": "Does NOT suggest explicitly calling os.Remove before log.Fatal as the primary fix"}
]
},
{
"id": 18,
"name": "time-equal-not-double-equals",
"description": "Tests the time.Time == vs .Equal() gotcha from common-go-bugs",
"prompt": "My Go test comparing time values fails intermittently. I store a time.Time in the database and read it back, then compare with ==. The test passes when I use a fixed time but fails when I use time.Now():\n\n```go\nt1 := time.Now()\nsaveToDatabase(t1)\nt2 := loadFromDatabase()\nassert.True(t, t1 == t2) // fails!\n```\nThe times represent the same instant. Why does == fail?",
"trap": "Model suggests the database truncates nanoseconds or timezone differences, not the monotonic clock component",
"assertions": [
{"id": "18.1", "text": "Identifies that time.Now() includes a monotonic clock reading that database serialization strips"},
{"id": "18.2", "text": "Explains that == compares all fields including the monotonic component, so it can fail for equal instants"},
{"id": "18.3", "text": "Recommends using .Equal() which ignores the monotonic clock"},
{"id": "18.4", "text": "Alternatively mentions t.Round(0) to strip the monotonic reading before comparison or storage"},
{"id": "18.5", "text": "Does NOT primarily blame database precision or timezone differences"}
]
},
{
"id": 19,
"name": "sql-rows-must-be-closed",
"description": "Tests the sql.Rows close requirement and connection leak from common-go-bugs",
"prompt": "My Go service starts failing with 'too many connections' after running for a few hours under load. Database queries start timing out. The code:\n\n```go\nfunc getActiveUsers(db *sql.DB) ([]User, error) {\n rows, err := db.Query(\"SELECT id, name FROM users WHERE active = true\")\n if err != nil {\n return nil, err\n }\n var users []User\n for rows.Next() {\n var u User\n rows.Scan(&u.ID, &u.Name)\n users = append(users, u)\n }\n return users, nil\n}\n```",
"trap": "Model suggests increasing the connection pool size or adding connection timeout, rather than finding the missing rows.Close()",
"assertions": [
{"id": "19.1", "text": "Identifies the missing defer rows.Close() after the error check"},
{"id": "19.2", "text": "Explains that unclosed sql.Rows holds the database connection until garbage collection"},
{"id": "19.3", "text": "Adds defer rows.Close() immediately after the err check"},
{"id": "19.4", "text": "Also notes the missing rows.Err() check after the loop"},
{"id": "19.5", "text": "Does NOT primarily suggest increasing connection pool size"}
]
},
{
"id": 20,
"name": "copying-sync-types-value-receiver",
"description": "Tests the sync type copying bug from common-go-bugs",
"prompt": "My Go concurrent counter gives wrong results. Multiple goroutines call Increment() but the final count is always 0 or some small number, never the expected total. The race detector doesn't fire. What's wrong?\n\n```go\ntype Counter struct {\n mu sync.Mutex\n count int\n}\n\nfunc (c Counter) Increment() {\n c.mu.Lock()\n c.count++\n c.mu.Unlock()\n}\n\nfunc (c Counter) Count() int {\n c.mu.Lock()\n defer c.mu.Unlock()\n return c.count\n}\n```",
"trap": "Model suggests the mutex isn't working or suggests using atomic instead, without identifying the value receiver as the root cause",
"assertions": [
{"id": "20.1", "text": "Identifies that value receivers (c Counter) copy the entire struct including the Mutex on every call"},
{"id": "20.2", "text": "Explains that each call operates on a copy — increments are lost and the mutex is duplicated"},
{"id": "20.3", "text": "Recommends changing to pointer receivers (c *Counter)"},
{"id": "20.4", "text": "Notes that go vet can detect copied sync types"},
{"id": "20.5", "text": "Explains this applies to ALL sync types (Mutex, RWMutex, WaitGroup, Once, etc.)"}
]
},
{
"id": 21,
"name": "pprof-production-security",
"description": "Tests the pprof security requirement: never expose unauthenticated in production",
"prompt": "I need to add CPU and memory profiling to my Go production web service. I'll just add `import _ \"net/http/pprof\"` and expose it on the main HTTP port. What's the simplest way to set this up?",
"trap": "Model provides the simple blank-import pattern without security warnings, exposing pprof publicly",
"assertions": [
{"id": "21.1", "text": "Warns that pprof endpoints MUST be protected — never exposed publicly without authentication"},
{"id": "21.2", "text": "Recommends basic auth or similar authentication on pprof endpoints"},
{"id": "21.3", "text": "Suggests running pprof on a separate port (not the main HTTP port) or localhost only"},
{"id": "21.4", "text": "Recommends toggling pprof via an environment variable (e.g., PPROF_ENABLED)"},
{"id": "21.5", "text": "Explains the risk: pprof leaks goroutine stacks, memory contents, and can be used for DoS"}
]
},
{
"id": 22,
"name": "godebug-gc-tracing-interpretation",
"description": "Tests GODEBUG gctrace interpretation from diagnostic-tools reference",
"prompt": "My Go service is experiencing periodic latency spikes. I enabled GC tracing with GODEBUG=gctrace=1 and see this output:\n```\ngc 456 @120.5s 18%: 2.1+45+1.2 ms clock, 16+12/45/8 ms cpu, 1024->900->500 MB\n```\nWhat does this tell me and is there a problem?",
"trap": "Model focuses only on the heap sizes and misses the 18% GC CPU overhead as the key signal",
"assertions": [
{"id": "22.1", "text": "Identifies that 18% GC CPU overhead is significantly high (threshold is >10%)"},
{"id": "22.2", "text": "Explains the heap size breakdown as heap at GC start, heap at GC end, and live heap"},
{"id": "22.3", "text": "Identifies the large pause times (45ms) as a likely cause of the latency spikes"},
{"id": "22.4", "text": "Suggests the application is over-allocating and recommends investigating allocation patterns"},
{"id": "22.5", "text": "Recommends using pprof heap/alloc profiling to find hot allocation sites"}
]
},
{
"id": 23,
"name": "research-codebase-not-just-diff",
"description": "Tests Golden Rule #6: trace callers and check upstream validation before flagging a bug",
"prompt": "During code review, I found this Go function. It looks like it has a bug — it doesn't validate that `id` is positive before using it as a slice index:\n\n```go\nfunc getItem(items []Item, id int) Item {\n return items[id]\n}\n```\nShould I flag this as a bug?",
"trap": "Model immediately flags it as a bug and suggests adding bounds checking, without considering that callers might already validate",
"assertions": [
{"id": "23.1", "text": "Recommends checking the callers first before flagging the bug"},
{"id": "23.2", "text": "Suggests using Grep or similar to find all call sites of getItem"},
{"id": "23.3", "text": "Notes that upstream code may validate the id (e.g., parsing from uint, bounds checking, positive-only input)"},
{"id": "23.4", "text": "Advises that if callers validate, the severity is reduced but may still warrant a defensive check"},
{"id": "23.5", "text": "Mentions adding an inline comment documenting the assumption if upstream guarantees exist"}
]
},
{
"id": 24,
"name": "flaky-test-diagnosis-methodology",
"description": "Tests flaky test debugging methodology from testing-debug reference",
"prompt": "One of our Go tests fails about 1 in 20 runs in CI but I can never reproduce it locally. The test creates a temp file, writes data, reads it back, and compares. How do I debug this?",
"trap": "Model suggests adding retry logic or skipping the test in CI, rather than systematic flaky test diagnosis",
"assertions": [
{"id": "24.1", "text": "Recommends running with -count=100 to reproduce locally"},
{"id": "24.2", "text": "Suggests using -shuffle=on to check for test order dependence"},
{"id": "24.3", "text": "Mentions running with -race to check for data races"},
{"id": "24.4", "text": "Suggests using t.TempDir() instead of shared temp directories to avoid file system pollution"},
{"id": "24.5", "text": "Considers shared mutable state between tests as a potential cause"},
{"id": "24.6", "text": "Does NOT suggest retry logic or skipping the test as a solution"}
]
},
{
"id": 25,
"name": "defense-in-depth-after-fix",
"description": "Tests Step 10 of methodology: multi-layer defense after fixing a bug",
"prompt": "I fixed a bug where user-submitted file paths could traverse outside the upload directory using ../. The fix adds filepath.Clean and a strings.HasPrefix check. Is this fix complete?",
"trap": "Model says the fix looks good without recognizing that Clean+HasPrefix is not robust confinement and without considering defense-in-depth — os.Root, safer lexical fallback, logging, and test coverage",
"assertions": [
{"id": "25.1", "text": "States that filepath.Clean plus strings.HasPrefix is not robust confinement"},
{"id": "25.2", "text": "Recommends adding a test that specifically verifies the path traversal is blocked"},
{"id": "25.3", "text": "Suggests adding logging or metrics to detect future traversal attempts (observability)"},
{"id": "25.4", "text": "Considers multiple validation layers — not just one check"},
{"id": "25.5", "text": "Recommends os.Root for Go 1.24+ or a filepath.IsLocal/filepath.Rel fallback for older targets"}
]
},
{
"id": 26,
"name": "escalation-protocol-three-failed-attempts",
"description": "Tests the escalation protocol: after 3 failed fix attempts, step back and question architecture",
"prompt": "I've tried fixing this Go data processing bug 4 times now. Each fix reveals a new problem — first the data was truncated, then the order was wrong, then duplicates appeared, now there's a memory leak. The code processes events from a Kafka topic and aggregates them in a map. What should I try next?",
"trap": "Model suggests a 5th specific fix (more memory management, deduplication logic, etc.) instead of stepping back to question the architecture",
"assertions": [
{"id": "26.1", "text": "Recognizes the pattern of cascading failures as a red flag — each fix reveals a new problem"},
{"id": "26.2", "text": "Recommends stepping back to question the overall design/architecture rather than trying another fix"},
{"id": "26.3", "text": "Suggests re-reading the code from scratch with fresh eyes"},
{"id": "26.4", "text": "Considers whether the current abstraction is fundamentally sound"},
{"id": "26.5", "text": "Does NOT immediately suggest a 5th specific patch to the existing code"}
]
},
{
"id": 27,
"name": "git-bisect-for-regression",
"description": "Tests the methodology step 1: using git bisect to find breaking commit for regressions",
"prompt": "A feature that was working last week is now broken in our Go service. I'm not sure which commit broke it. There have been about 50 commits since it last worked. How should I find what changed?",
"trap": "Model suggests reading through all 50 commit diffs manually or running tests on HEAD",
"assertions": [
{"id": "27.1", "text": "Recommends git bisect to binary-search for the breaking commit"},
{"id": "27.2", "text": "Shows the git bisect start / git bisect bad / git bisect good workflow"},
{"id": "27.3", "text": "Mentions that bisect can be automated with a test command (git bisect run go test -run TestBroken ./...)"},
{"id": "27.4", "text": "Notes this narrows 50 commits to ~6 steps (log2(50))"},
{"id": "27.5", "text": "Does NOT suggest manually reading all 50 commit diffs"}
]
},
{
"id": 28,
"name": "check-external-dependencies-first",
"description": "Tests Step 4 of methodology: verify external components before assuming code bug",
"prompt": "My Go service started returning 'connection refused' errors for API calls to a third-party payment service. This worked fine yesterday. Nothing in our code changed (I checked git log). Where should I look?",
"trap": "Model starts investigating Go HTTP client code or TLS configuration instead of checking the external service first",
"assertions": [
{"id": "28.1", "text": "Suggests checking the external payment service health/status first (curl, health endpoint)"},
{"id": "28.2", "text": "Recommends checking DNS resolution (dig or nslookup)"},
{"id": "28.3", "text": "Suggests checking network connectivity (nc, telnet, or similar to the port)"},
{"id": "28.4", "text": "Considers environment-specific causes: expired credentials, DNS changes, firewall rules, certificate rotation"},
{"id": "28.5", "text": "Does NOT start by investigating Go code since nothing changed in the codebase"}
]
},
{
"id": 29,
"name": "observability-tools-before-code-dive",
"description": "Tests Step 5 of methodology: check observability data before diving into code",
"prompt": "Our Go microservice started returning 500 errors about 2 hours ago. I want to start reading the code to find the bug. Where should I start looking in the codebase?",
"trap": "Model jumps straight into reading handler code or error paths instead of suggesting checking observability tools first",
"assertions": [
{"id": "29.1", "text": "Recommends checking monitoring/observability tools BEFORE diving into code"},
{"id": "29.2", "text": "Asks what monitoring tools are available (Prometheus, Datadog, Sentry, ELK, etc.)"},
{"id": "29.3", "text": "Suggests checking error rate metrics, latency dashboards, or log aggregation"},
{"id": "29.4", "text": "Mentions specific things to look for: what changed 2 hours ago (deploy, config change, traffic spike)"},
{"id": "29.5", "text": "Does NOT immediately start reading source code files"}
]
},
{
"id": 30,
"name": "integer-conversion-silent-truncation",
"description": "Tests integer conversion truncation from common-go-bugs",
"prompt": "My Go code converts user-provided int64 values to int32 for a legacy protocol. It works for most values but produces wrong results for large numbers. The conversion is `int32(bigValue)`. Is there a Go function to convert safely?",
"trap": "Model suggests casting with a simple function or using math.MinInt32/MaxInt32 incorrectly",
"assertions": [
{"id": "30.1", "text": "Explains that Go integer conversions silently truncate without any error or warning"},
{"id": "30.2", "text": "Shows bounds checking before conversion: compare against math.MinInt32 and math.MaxInt32"},
{"id": "30.3", "text": "Returns an error when the value overflows instead of silently truncating"},
{"id": "30.4", "text": "Notes there is no built-in safe conversion function — you must check bounds manually"},
{"id": "30.5", "text": "Mentions this is especially dangerous for external/user-provided data"}
]
},
{
"id": 31,
"name": "init-ordering-fragile",
"description": "Tests the init() ordering fragility from common-go-bugs",
"prompt": "My Go program panics during startup with a nil pointer. I have an init() function that opens a database connection using a config value from another init() in a different file. It works in development but fails in CI. Could the init order be different?",
"trap": "Model suggests ensuring the config init file is imported first or adding a side-effect import, rather than recommending explicit initialization",
"assertions": [
{"id": "31.1", "text": "Confirms that init() ordering across files depends on filename alphabetical order and can change when files are added"},
{"id": "31.2", "text": "Explains this makes init() dependencies fragile and hard to debug"},
{"id": "31.3", "text": "Recommends replacing init() with explicit initialization in main()"},
{"id": "31.4", "text": "Shows the pattern: cfg := loadConfig(); db := setupDatabase(cfg); startServer(db)"},
{"id": "31.5", "text": "States init() should only be used for truly self-contained setup (registering drivers, codecs)"}
]
},
{
"id": 32,
"name": "goroutine-leak-detection-methodology",
"description": "Tests goroutine leak diagnosis from concurrency-debug reference",
"prompt": "My Go service's memory usage grows slowly over days. CPU is normal. There's no obvious memory leak in heap profiling. What else could cause the slow growth?",
"trap": "Model focuses only on heap analysis and misses goroutine leaks as a major cause of slow memory growth",
"assertions": [
{"id": "32.1", "text": "Suggests checking goroutine count (runtime.NumGoroutine or pprof goroutine profile)"},
{"id": "32.2", "text": "Explains that goroutine leaks cause slow memory growth without appearing in heap profiles"},
{"id": "32.3", "text": "Recommends using the pprof goroutine endpoint with ?debug=2 for human-readable stack dumps"},
{"id": "32.4", "text": "Lists common causes: unclosed channels, missing context cancellation, forgotten response body close"},
{"id": "32.5", "text": "Suggests goleak for detection in tests"}
]
},
{
"id": 33,
"name": "production-capture-before-restart",
"description": "Tests the production debugging checklist: capture profiles BEFORE restarting",
"prompt": "Our Go production service is consuming 4GB of memory and responding slowly. We need to fix this urgently. Should I restart the service first to restore normal operation?",
"trap": "Model agrees to restart first to restore service, losing all diagnostic information",
"assertions": [
{"id": "33.1", "text": "Recommends capturing profiles (heap, goroutine, CPU) BEFORE restarting"},
{"id": "33.2", "text": "Explains that restarting destroys the evidence needed to diagnose the root cause"},
{"id": "33.3", "text": "Lists specific profiles to capture: heap, goroutine dump (?debug=2), CPU (30s), mutex"},
{"id": "33.4", "text": "Also suggests capturing system metrics (file descriptors, socket state, process info)"},
{"id": "33.5", "text": "Only after capturing all evidence should the service be restarted if needed"}
]
},
{
"id": 34,
"name": "lock-contention-diagnosis",
"description": "Tests lock contention diagnosis from performance-debug reference",
"prompt": "My Go web server shows high CPU usage but low throughput. Adding more cores doesn't help — performance stays flat. The profiler shows most time in runtime.semacquire. What's going on?",
"trap": "Model suggests the workload is CPU-bound and recommends algorithmic optimization, rather than identifying lock contention",
"assertions": [
{"id": "34.1", "text": "Identifies runtime.semacquire as a signal of lock contention, not CPU computation"},
{"id": "34.2", "text": "Recommends enabling mutex profiling with runtime.SetMutexProfileFraction(1)"},
{"id": "34.3", "text": "Recommends enabling block profiling with runtime.SetBlockProfileRate(1)"},
{"id": "34.4", "text": "Suggests using pprof mutex and block profiles to find the contended locks"},
{"id": "34.5", "text": "Lists solutions: reduce critical section, sharding, RWMutex, atomic operations"}
]
},
{
"id": 35,
"name": "race-detector-not-reasoning",
"description": "Tests Golden Rule #3: never reason about concurrency — use the race detector",
"prompt": "I'm reviewing Go code that has goroutines sharing a struct. I don't see any obvious race conditions — the goroutines seem to access different fields. Is this safe?\n\n```go\ntype Stats struct {\n RequestCount int64\n ErrorCount int64\n LastUpdated time.Time\n}\n\nfunc (s *Stats) RecordRequest() {\n s.RequestCount++\n}\n\nfunc (s *Stats) RecordError() {\n s.ErrorCount++\n}\n```",
"trap": "Model reasons through the code and concludes it looks safe because different goroutines access different fields",
"assertions": [
{"id": "35.1", "text": "Does NOT conclude safety based on code reasoning alone"},
{"id": "35.2", "text": "Recommends running go test -race to verify — never trust visual inspection for concurrency"},
{"id": "35.3", "text": "Identifies that ++ is not atomic — RequestCount++ and ErrorCount++ are read-modify-write operations"},
{"id": "35.4", "text": "Recommends using atomic.Int64 or sync.Mutex to protect the fields"},
{"id": "35.5", "text": "Notes that even different fields on the same struct can race if accessed from different goroutines without synchronization"}
]
},
{
"id": 36,
"name": "filepath-join-path-traversal",
"description": "Tests the filepath.Join path traversal from common-go-bugs",
"prompt": "I'm building a Go file server. I use filepath.Join to safely combine the base directory with the user-requested path. Is this implementation secure?\n\n```go\nfunc serveFile(w http.ResponseWriter, r *http.Request) {\n path := filepath.Join(\"/srv/files\", r.URL.Path)\n http.ServeFile(w, r, path)\n}\n```",
"trap": "Model says filepath.Join handles path cleaning and the code is safe",
"assertions": [
{"id": "36.1", "text": "Identifies that filepath.Join does NOT prevent path traversal"},
{"id": "36.2", "text": "Shows that input like '../../etc/passwd' resolves to '/etc/passwd' after Join"},
{"id": "36.3", "text": "Recommends os.Root for Go 1.24+ user-controlled filesystem access"},
{"id": "36.4", "text": "For older Go targets, shows a fallback using filepath.IsLocal plus filepath.Rel with separator-aware checks"},
{"id": "36.5", "text": "Does NOT present filepath.Clean plus strings.HasPrefix as a complete traversal defense"}
]
},
{
"id": 37,
"name": "time-after-in-loop-allocation-churn",
"description": "Tests the repeated time.After in loop allocation-churn pattern from code-review-flags and concurrency-debug",
"prompt": "My Go worker processes messages from a channel with a timeout. Memory usage grows over time even though messages are processed correctly. Here's the code:\n\n```go\nfor {\n select {\n case msg := <-incoming:\n process(msg)\n case <-time.After(30 * time.Second):\n log.Println(\"idle timeout\")\n return\n }\n}\n```",
"trap": "Model suggests the message processing is leaking memory, without identifying repeated time.After allocation churn and reset semantics as the likely issue",
"assertions": [
{"id": "37.1", "text": "Identifies that time.After creates a new timer on every loop iteration"},
{"id": "37.2", "text": "Explains this causes allocation churn and was a leak-like pattern on older Go versions before Go 1.23 timer GC improvements"},
{"id": "37.3", "text": "Recommends using time.NewTimer with Reset() or time.NewTicker instead"},
{"id": "37.4", "text": "Shows the correct pattern with a reusable timer and defer timer.Stop()"},
{"id": "37.5", "text": "Does NOT suggest heap profiling as the first diagnostic step for this known pattern"}
]
}
]
Code Review Red Flags
If you see these in code review, flag them:
| Pattern | Why It's Bad |
|---|---|
result, _ := doSomething() | Silent error — mystery bugs later |
go func() { }() without context | Can't cancel, leaks goroutine |
| Channel without close | Goroutine leak when sender exits |
time.After in hot loop | Repeated timer allocation/churn; use a reusable timer when reset semantics matter |
| Global map without mutex | Data race |
defer inside hot loop | Deferred calls pile up until return |
json.Marshal in hot path | Expensive, causes GC pressure |
for range without ok check | Misses channel close |
var err *MyError; return err | Interface nil gotcha |
http.Get without timeout | Default client has no timeout |
fmt.Errorf("...: %v", err) | Use %w to preserve error chain |
:= shadowing outer err | Inner err is a new variable, outer stays nil |
func (c Counter) Lock() | Value receiver copies sync types |
wg.Add(1) inside goroutine | Race: Wait() may return before Add() |
http.Error(...) without return | Handler keeps executing after error |
iota starting at 0 for enums | Zero value ambiguous with first constant |
strings.Trim(s, "prefix") | Strips char set, not substring |
log.Fatal(err) in func w/ defer | os.Exit skips all deferred cleanup |
t1 == t2 for time.Time | Use .Equal() — monotonic clock differs |
rows, _ := db.Query(...) no Close | Leaks database connections |
ch <- val after close(ch) | Panics — only sender should close |
select { default: } in loop | Busy loop — burns CPU without blocking |
int32(bigInt64) | Silent truncation — no overflow check |
filepath.Join(base, userInput) | Doesn't prevent ../ path traversal |
regexp.MustCompile in handler | Recompiles every call — move to package var |
fallthrough in switch | Executes next case unconditionally |
Common Go Bugs
→ See samber/cc-skills-golang@golang-safety skill for in-depth nil, slice, and map safety patterns.
Nil Pointer Dereference
Pointers from external sources MUST be checked before dereferencing.
The most common Go panic. The stack trace tells you the exact line.
// 1. Uninitialized struct field
type Server struct {
logger *log.Logger // nil if not set in constructor
}
// 2. Unchecked error return — if err != nil, val may be nil/zero
val, err := doSomething()
val.Method() // panic if doSomething returned nil val with an error
// 3. Map lookup returns zero value
m := map[string]*Config{}
cfg := m["missing"] // cfg is nil
cfg.Timeout // panic
// 4. Type assertion without comma-ok
var i interface{} = "hello"
n := i.(int) // panic
n, ok := i.(int) // ok == false, no panicInterface Nil Gotcha
NEVER compare an interface to nil when it may contain a typed nil pointer.
A typed nil pointer inside an interface is not a nil interface:
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
func doWork() error {
var err *MyError // typed nil pointer
return err // returns non-nil interface containing nil pointer!
}
func main() {
if err := doWork(); err != nil {
// This EXECUTES — the interface is non-nil
fmt.Println(err) // panic: nil pointer in Error()
}
}
// FIX: return nil explicitly, not a typed nil variable
func doWork() error {
return nil
}Variable Shadowing with :=
The := short declaration creates a new variable in the inner scope instead of assigning to the outer one. Especially dangerous when shadowing err, because error handling silently breaks.
// BAD
func doWork() error {
var err error
if condition {
result, err := someFunc() // BUG: new err variable, doesn't set outer one
if err != nil {
return err
}
process(result)
}
return err // always nil — inner err was a different variable
}
// GOOD
func doWork() error {
var err error
if condition {
var result ResultType
result, err = someFunc() // assigns to outer err
if err != nil {
return err
}
process(result)
}
return err
}Detect: run the golang.org/x/tools/go/analysis/passes/shadow analyzer through your lint setup. The old shadow flag is not part of standard go vet.
Slice and Map Gotchas
// 1. Nil map write panics
var m map[string]int
m["key"] = 1 // panic: assignment to entry in nil map
// FIX: m := make(map[string]int)
// Note: nil map reads are fine — they return zero value
// 2. Append may share underlying array
a := []int{1, 2, 3}
b := a[:2]
b = append(b, 99) // overwrites a[2]!
// FIX: full slice expression — b := a[:2:2] to limit capacity
// 3. Range variable capture in goroutine (Go < 1.22)
for _, v := range items {
go func() {
process(v) // v is shared, will likely be last element
}()
}
// FIX: pass as argument
for _, v := range items {
go func(v Item) { process(v) }(v)
}
// In Go 1.22+, loop variables are per-iteration (no fix needed)Defer Gotchas
// 1. Arguments evaluated immediately
x := 1
defer fmt.Println(x) // prints 1, not 2
x = 2
// 2. Defer in loop — doesn't run until function returns
for _, f := range files {
file, _ := os.Open(f)
defer file.Close() // all Close() calls pile up until return
}
// FIX: wrap in closure
for _, f := range files {
func() {
file, _ := os.Open(f)
defer file.Close()
// use file
}()
}
// 3. Named return + defer interaction
func readFile() (err error) {
f, err := os.Open("file.txt")
if err != nil { return }
defer func() {
if closeErr := f.Close(); err == nil {
err = closeErr // modifies named return
}
}()
// ...
return nil
}Error Handling Pitfalls
Silent error swallowing is the single most common source of "mysterious" bugs:
// BAD — silent failure
result, _ := doSomething()
json.Unmarshal(data, &config)
http.ListenAndServe(":8080", nil)
// GOOD — handle or propagate
result, err := doSomething()
if err != nil {
return fmt.Errorf("doSomething: %w", err)
}Find ignored errors:
go vet ./...
# More thorough
go get -tool github.com/kisielk/errcheck@latest
go tool errcheck ./...Error wrapping — use `%w`, not `%v`:
return fmt.Errorf("reading config from %s: %v", path, err) // BAD — loses error chain
return fmt.Errorf("reading config from %s: %w", path, err) // GOOD — preserves Is/As
// Check for specific errors — use errors.Is, not ==
if err == sql.ErrNoRows { ... } // BAD — breaks if wrapped
if errors.Is(err, sql.ErrNoRows) { ... } // GOOD — traverses chain
// Extract typed errors
var pathErr *os.PathError
if errors.As(err, &pathErr) { ... }Context Misuse
// 1. Forgetting to cancel — leaks goroutines
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
// Missing: defer cancel()
// 2. Using background context when you should propagate
go doWork(context.Background()) // BAD — can't cancel from parent
go doWork(ctx) // GOOD — respects parent cancellation
// 3. Not checking context error
err := doWork(ctx)
if err != nil {
// Distinguish timeout from other errors
if ctx.Err() == context.DeadlineExceeded {
log.Printf("operation timed out")
} else if ctx.Err() == context.Canceled {
log.Printf("operation cancelled")
} else {
log.Printf("operation failed: %v", err)
}
}
// 4. Background work outliving request context
func handler(w http.ResponseWriter, r *http.Request) {
// BAD — background work uses request context that cancels when client disconnects
go processAsync(r.Context(), data)
// GOOD — derive a new context for background work (Go 1.21+)
bgCtx := context.WithoutCancel(r.Context())
go processAsync(bgCtx, data)
}Concurrent Map Read/Write (Fatal)
Maps MUST NOT be accessed concurrently without synchronization.
Unlike most Go runtime errors, a concurrent map read/write is a fatal error — it cannot be caught with `recover()` and crashes the entire process. Hard to catch in tests because it depends on timing.
// BAD — fatal: concurrent map read and map write
m := make(map[string]int)
go func() { m["key"] = 1 }() // concurrent write
go func() { _ = m["key"] }() // concurrent read — fatal!
// GOOD — protect with mutex
var mu sync.RWMutex
m := make(map[string]int)
go func() { mu.Lock(); m["key"] = 1; mu.Unlock() }()
go func() { mu.RLock(); _ = m["key"]; mu.RUnlock() }()
// Or use sync.Map for read-heavy workloads with stable key setsDetect: go test -race ./... — always run in CI.
Copying sync Types
Sync types MUST NEVER be copied — use pointer receivers and pass by pointer.
All sync types (Mutex, RWMutex, WaitGroup, Once, Cond, Map, Pool) must not be copied. Copying them via value receivers, function arguments, or struct assignment silently breaks synchronization.
// BAD — value receiver copies the Mutex
type Counter struct {
mu sync.Mutex
count int
}
func (c Counter) Increment() { // BUG: copies mutex on every call
c.mu.Lock()
c.count++
c.mu.Unlock()
}
// GOOD — pointer receiver
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}Detect: go vet detects mutex copies. Apply to all sync types.
WaitGroup.Add Inside Goroutine
If wg.Add(1) is called inside the goroutine instead of before it, wg.Wait() may return before all goroutines start — a race condition that passes tests most of the time but fails intermittently.
// BAD
var wg sync.WaitGroup
for i := 0; i < n; i++ {
go func() {
wg.Add(1) // BUG: may run after wg.Wait() returns
defer wg.Done()
doWork()
}()
}
wg.Wait()
// GOOD
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1) // called BEFORE launching the goroutine
go func() {
defer wg.Done()
doWork()
}()
}
wg.Wait()Missing Return After HTTP Error Response
After writing an error with http.Error(), execution continues. This can cause double writes, corrupted responses, or executing logic that should have been skipped.
// BAD
func handler(w http.ResponseWriter, r *http.Request) {
if !authorized(r) {
http.Error(w, "Forbidden", http.StatusForbidden)
// BUG: missing return — handler keeps executing
}
doSensitiveAction(r)
}
// GOOD
func handler(w http.ResponseWriter, r *http.Request) {
if !authorized(r) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
doSensitiveAction(r)
}JSON Pitfalls
Numbers into interface{} become float64
When unmarshaling into map[string]interface{} or interface{}, all JSON numbers become float64. Type-asserting to int panics. Large integers (> 2^53) silently lose precision.
// BAD
var result map[string]interface{}
json.Unmarshal([]byte(`{"id": 1234567890123456789}`), &result)
id := result["id"].(int) // PANIC: it's float64, not int
// GOOD — use typed struct (preferred)
type Response struct {
ID int64 `json:"id"`
}
// GOOD — use json.Number when you must use interface{}
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var result map[string]interface{}
dec.Decode(&result)
id, _ := result["id"].(json.Number).Int64()Unexported fields silently ignored
Fields starting with lowercase are invisible to encoding/json. Marshal produces empty output, unmarshal skips them — no error in either case.
// BAD
type User struct {
name string `json:"name"` // unexported — silently ignored!
email string `json:"email"` // unexported — silently ignored!
}
u := User{name: "Alice", email: "alice@example.com"}
data, _ := json.Marshal(u) // data is "{}" — no error
// GOOD
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}Detect: go vet warns when unexported fields have JSON struct tags.
strings.Trim vs strings.TrimPrefix
strings.Trim treats its second argument as a set of characters to strip from both ends, not as a substring. This over-trims unexpectedly.
// BAD
s := strings.Trim("application/json", "application/")
// Result: "js" — stripped all chars in set {a,p,l,i,c,t,o,n,/} from both ends!
// GOOD
s := strings.TrimPrefix("application/json", "application/")
// Result: "json"Use strings.TrimPrefix/strings.TrimSuffix to remove substrings. Only use strings.Trim when you intend to strip a set of characters.
String Length and Indexing
len() on strings returns bytes, not characters. Indexing returns a byte. For multi-byte UTF-8 characters, this gives wrong counts and corrupts data when slicing.
s := "Hello, 世界"
fmt.Println(len(s)) // 13 (bytes), not 9 (characters)
fmt.Println(s[:8]) // "Hello, \xe4" — corrupted! cuts a multi-byte rune
// FIX: use utf8.RuneCountInString for character count
fmt.Println(utf8.RuneCountInString(s)) // 9
// FIX: convert to []rune for character-based slicing
runes := []rune(s)
fmt.Println(string(runes[:8])) // "Hello, 世"
// FIX: use for-range to iterate over characters (runes), not bytes
for _, r := range s { ... } // iterates runesbreak in select/switch Inside for Loop
A bare break inside a select or switch that is inside a for loop only exits the select/switch, not the loop.
// BAD
for {
select {
case msg := <-ch:
if msg == "quit" {
break // BUG: only breaks the select, loop continues forever
}
process(msg)
}
}
// GOOD — use labeled break
loop:
for {
select {
case msg := <-ch:
if msg == "quit" {
break loop // breaks the for loop
}
process(msg)
}
}Enum Zero Value with iota
When iota starts at 0, the zero value of the type (from uninitialized variables, zero-value struct fields, or missing JSON fields) is indistinguishable from the first constant.
// BAD
type Status int
const (
Active Status = iota // 0 — same as zero value!
Inactive // 1
)
type User struct {
Status Status // zero value is Active — but was it intentional?
}
// GOOD — reserve 0 for "unknown"
type Status int
const (
StatusUnknown Status = iota // 0 — explicit unset sentinel
StatusActive // 1
StatusInactive // 2
)recover() Only Works in the Same Goroutine
recover() can only catch panics in the goroutine where it's deferred. A panic in a child goroutine will crash the entire program — no parent goroutine can catch it.
// BAD — recover() in main cannot catch panic in child goroutine
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r) // NEVER REACHED
}
}()
go func() {
panic("crash!") // crashes the whole program
}()
time.Sleep(time.Second)
}
// GOOD — each goroutine must recover its own panics
func main() {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine recovered: %v", r)
}
}()
panic("crash!") // recovered within this goroutine
}()
time.Sleep(time.Second)
}os.Exit Skips Deferred Functions
os.Exit terminates the process immediately. No deferred functions run — cleanup, flush, and close operations are skipped. log.Fatal calls os.Exit(1) internally and has the same problem.
// BAD — deferred cleanup never runs
func main() {
f, _ := os.Create("data.tmp")
defer f.Close() // NEVER RUNS
defer os.Remove(f.Name()) // NEVER RUNS
if err := process(); err != nil {
log.Fatal(err) // calls os.Exit(1) — skips all defers!
}
}
// GOOD — return from main instead, or restructure so defers run
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1) // defers in run() already ran when it returned
}
}
func run() error {
f, _ := os.Create("data.tmp")
defer f.Close()
return process()
}time.Time Comparison: == vs .Equal()
time.Time includes a monotonic clock reading. Two time.Time values representing the same instant may not be == if one has a monotonic component and the other doesn't (e.g., one from time.Now(), the other deserialized from JSON/database).
// BAD — may fail even for the same instant
t1 := time.Now()
data, _ := t1.MarshalJSON()
var t2 time.Time
t2.UnmarshalJSON(data)
fmt.Println(t1 == t2) // false! t1 has monotonic, t2 doesn't
// GOOD — .Equal() ignores monotonic clock
fmt.Println(t1.Equal(t2)) // true
// Also: strip monotonic explicitly when storing/comparing
t1 = t1.Round(0) // strips monotonic readingsql.Rows Must Be Closed
sql.Rows MUST call rows.Close() — always defer it immediately after the query.
Forgetting to close sql.Rows leaks database connections. The connection is held until Rows is garbage collected, but under load the connection pool exhausts first.
// BAD — connection leak if rows aren't closed
rows, err := db.Query("SELECT id FROM users")
if err != nil { return err }
for rows.Next() {
// ...
}
// rows never closed — connection leak!
// GOOD — always defer Close
rows, err := db.Query("SELECT id FROM users")
if err != nil { return err }
defer rows.Close()
for rows.Next() {
// ...
}
if err := rows.Err(); err != nil { // don't forget to check rows.Err()
return err
}Also: use db.QueryRow() for single-row queries and db.Exec() for non-SELECT statements (INSERT, UPDATE, DELETE). Using db.Query() for non-SELECT leaks connections because the returned Rows is never iterated/closed.
Writing to a Closed Channel Panics
Sending to a closed channel panics. Reading from a closed channel returns the zero value immediately (with ok == false).
// BAD — panic: send on closed channel
ch := make(chan int, 1)
close(ch)
ch <- 1 // panic!
// GOOD — only the sender should close, never the receiver
// Use a done channel or context to signal completion
func producer(ch chan<- int, done <-chan struct{}) {
defer close(ch)
for i := 0; ; i++ {
select {
case ch <- i:
case <-done:
return
}
}
}Rule of thumb: Only the sender closes the channel. If multiple senders, use a sync.Once or coordinate with a sync.WaitGroup.
Closed Channel in select Causes Busy Loop
A closed channel is always ready to receive (returns zero value). In a select, this causes the case to fire continuously — a CPU-burning busy loop.
// BAD — after ch is closed, this loops at 100% CPU
for {
select {
case v := <-ch: // fires continuously after ch closes
process(v) // processes zero values forever
case <-done:
return
}
}
// GOOD — nil the channel after it closes
for {
select {
case v, ok := <-ch:
if !ok {
ch = nil // nil channel blocks forever in select — disables this case
continue
}
process(v)
case <-done:
return
}
}select with default Can Spin CPU
A select with a default case never blocks. Inside a for loop, this creates a busy-wait spin loop that burns CPU.
// BAD — spins at 100% CPU waiting for a message
for {
select {
case msg := <-ch:
process(msg)
default:
// runs immediately when ch has nothing — tight loop!
}
}
// GOOD — remove default to block until a message arrives
for {
select {
case msg := <-ch:
process(msg)
case <-ctx.Done():
return
}
}
// GOOD — if you need non-blocking check, add a small sleep or ticker
for {
select {
case msg := <-ch:
process(msg)
default:
time.Sleep(10 * time.Millisecond) // yield CPU
}
}Integer Conversion Silently Truncates
Go integer conversions don't check for overflow — they silently truncate. This is especially dangerous when converting from user input or external data.
// BAD — silent truncation
var big int64 = 256
small := int8(big)
fmt.Println(small) // 0 — silently overflowed!
var n int64 = math.MaxInt64
n32 := int32(n)
fmt.Println(n32) // -1 — silently wrapped!
// GOOD — check bounds before converting
func safeIntToInt32(n int64) (int32, error) {
if n < math.MinInt32 || n > math.MaxInt32 {
return 0, fmt.Errorf("value %d overflows int32", n)
}
return int32(n), nil
}filepath.Join Does Not Prevent Path Traversal
filepath.Join cleans the path (resolves ..) but doesn't prevent escaping the base directory. User-supplied paths can traverse outside the intended root.
// BAD — user can escape the base directory
base := "/srv/files"
userInput := "../../etc/passwd"
path := filepath.Join(base, userInput)
// path = "/etc/passwd" — escaped!
// GOOD (Go 1.24+) — confine access to the base directory
root, err := os.OpenRoot("/srv/files")
if err != nil {
return err
}
defer root.Close()
file, err := root.Open(userInput)
if err != nil {
return err
}
defer file.Close()For Go <1.24, use a lexical fallback only when os.Root is unavailable:
func safePath(base, userInput string) (string, error) {
if userInput == "" || filepath.IsAbs(userInput) || !filepath.IsLocal(userInput) {
return "", fmt.Errorf("invalid relative path: %q", userInput)
}
path := filepath.Join(base, userInput)
rel, err := filepath.Rel(base, path)
if err != nil {
return "", fmt.Errorf("checking path: %w", err)
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return "", fmt.Errorf("path traversal attempt: %s", userInput)
}
return path, nil
}Pointer Receiver Interface Satisfaction
A value of type T cannot satisfy an interface that requires methods with *T receivers. But *T satisfies interfaces requiring either T or *T methods.
type Sizer interface {
Size() int
}
type File struct{ size int }
func (f *File) Size() int { return f.size } // pointer receiver
var s Sizer
s = File{} // COMPILE ERROR: File does not implement Sizer (*File does)
s = &File{} // OK — *File has the Size method
// This is because the compiler can't always take the address of a value
// (e.g., map values, return values). Pointer receiver = pointer required.regexp.MustCompile in Hot Path
Long-lived regexp MUST be compiled once at package level — not inside functions called repeatedly. Short-lived regexp used once (e.g., in a CLI or test) are acceptable inline.
regexp.MustCompile compiles a regex every call. In a hot path (loop, HTTP handler), this is expensive and wasteful.
// BAD — recompiles regex on every call
func isEmail(s string) bool {
re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return re.MatchString(s)
}
// GOOD — compile once at package level
var emailRe = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
func isEmail(s string) bool {
return emailRe.MatchString(s)
}init() Ordering Is Fragile
init() functions run in source file order within a package, and in dependency order across packages. But relying on this order creates brittle, hard-to-debug initialization sequences. Multiple init() in the same file run top-to-bottom, but across files it's alphabetical by filename — adding a file can change the order.
// BAD — init() depends on another init() having run first
var db *sql.DB
func init() {
// Assumes config init() already ran — fragile!
db, _ = sql.Open("postgres", config.DatabaseURL)
}
// GOOD — use explicit initialization
func main() {
cfg := loadConfig()
db := setupDatabase(cfg)
startServer(db)
}Prefer explicit initialization in main() over init(). Use init() only for truly self-contained setup (registering drivers, codecs).
Map Iteration Order Is Random
Go deliberately randomizes map iteration order. Code that assumes a specific order will produce inconsistent results.
// BAD — output order is random every run
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
fmt.Printf("%s=%d ", k, v) // different order each time!
}
// GOOD — sort keys when order matters
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s=%d ", k, m[k])
}This is especially dangerous in tests (non-deterministic output comparison), serialization (non-deterministic JSON/output), and logging (confusing diffs).
fallthrough in switch Executes Unconditionally
Unlike C, Go's switch cases don't fall through by default. But when you explicitly use fallthrough, it executes the next case body unconditionally — it does not check the next case's condition.
// Surprising: fallthrough doesn't check the next condition
switch x := 5; {
case x > 10:
fmt.Println(">10")
fallthrough
case x > 0:
fmt.Println(">0")
fallthrough
case x < 0:
fmt.Println("<0") // EXECUTES even though 5 is not < 0!
}
// Output: >0, <0
// fallthrough is rarely needed. Prefer listing multiple values:
switch status {
case "active", "enabled":
enable()
}Compilation Issues
Module Problems
go clean -modcache # clean module cache
go mod download # re-download dependencies
go mod verify # verify dependencies
go mod tidy # tidy dependencies
go mod why <package> # why is this dependency here?CGO Issues
go env CGO_ENABLED # check CGO is enabled
export CGO_CFLAGS="-I/usr/local/include" # set CGO CFLAGS
# macOS: brew install pkg-config
# Ubuntu: apt install pkg-configVersion Mismatch
go version # check Go version
go mod edit -go=1.21 # set minimum required versionConcurrency Debugging
Goroutine Leaks
Symptoms: Memory slowly increasing, goroutine count growing, no obvious CPU spike.
Diagnosis:
Use pprof goroutine profile (see pprof.md) with ?debug=2 for human-readable output, then look for goroutines stuck in chan receive.
For Go 1.26 diagnostics, there is also an experimental goroutine leak profile. It is useful for production-oriented leak investigation, but is gated by GOEXPERIMENT=goroutineleakprofile; do not rely on it as default stable behavior.
curl http://localhost:6060/debug/pprof/goroutineleak?debug=2
go tool pprof http://localhost:6060/debug/pprof/goroutineleakKeep existing tools: go.uber.org/goleak in tests, runtime.NumGoroutine() for coarse monitoring, /debug/pprof/goroutine?debug=2 for stack dumps, and go test -race ./... for race checks.
// Programmatic monitoring — log goroutine count to detect leaks
go func() {
for {
log.Printf("goroutines: %d", runtime.NumGoroutine())
time.Sleep(3 * time.Second)
}
}()
// In tests, use goleak to detect goroutine leaks
// import "go.uber.org/goleak"
// func TestMain(m *testing.M) { goleak.VerifyTestMain(m) }Common causes:
// 1. Unclosed channel — goroutine blocks forever
// BAD
for {
job := <-jobs
process(job)
}
// GOOD
for {
select {
case job, ok := <-jobs:
if !ok { return }
process(job)
case <-ctx.Done():
return
}
}
// 2. Forgotten response body close — leaks HTTP connection
// Always defer resp.Body.Close() after HTTP calls.
// See production-debug.md for the correct pattern.
// 3. time.After in hot loop — allocates a new timer each iteration
// BAD
for {
select {
case <-time.After(time.Second):
do()
}
}
// GOOD — reuse a ticker for repeated intervals
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
do()
case <-ctx.Done():
return
}
}Race Conditions
Symptoms: Intermittent failures, "sometimes works sometimes doesn't", different results on different machines.
Diagnosis: Race conditions MUST be tested with the -race flag:
go test -race ./...
go run -race main.go
# Race detector slows code ~10x but finds data races reliablyCommon patterns:
- Shared map without mutex
- Shared variable without atomic
- Publishing reference before initialization
- go func() accessing outer variables without synchronization
Deadlocks
Symptoms: Program hangs, goroutines stuck in "chan receive" or "mutex lock".
Diagnosis:
curl http://localhost:6060/debug/pprof/goroutine?debug=2
# Or programmatic
runtime.Stack(buf, true)Common patterns:
1. Circular wait — A waits for B, B waits for A 2. Forgotten channel send — sender goroutine exited 3. Wrong lock order — always acquire locks in the same order
Diagnostic Tools
Runtime Diagnostics (GODEBUG)
Go documentation command
Use go doc, not go tool doc. Go 1.26 removed the old cmd/doc / go tool doc path.
GC Tracing
GODEBUG=gctrace=1 ./appOutput:
gc 123 @45.67s 4%: 0.8+10+0.3 ms clock, 6+5/10/0 ms cpu, 512->300->150 MB| Field | Meaning |
|---|---|
| 4% | GC CPU overhead (if >10%, over-allocating) |
| 512->300->150 MB | Heap at GC start -> heap at GC end -> live heap |
| Large pause | Allocation storm |
Scheduler Tracing
GODEBUG=schedtrace=1000,scheddetail=1 ./app| Signal | Meaning |
|---|---|
| runqueue high | CPU saturation, goroutines waiting |
| idleprocs=0 | Fully busy, at capacity |
| spinningthreads | Lock contention |
| threads > gomaxprocs | Blocking syscalls |
GOTRACEBACK
Get full stack traces on panic:
GOTRACEBACK=all ./app| Level | Shows |
|---|---|
none | No stack traces |
single | Current goroutine only (default) |
all | All goroutines (useful for deadlocks) |
system | All goroutines + runtime frames |
---
Delve Debugger
Installation
go install github.com/go-delve/delve/cmd/dlv@latestBasic Usage
dlv debug ./cmd/myapp # debug a program
dlv test ./mypackage # debug a test
dlv attach 12345 # attach to running process
dlv exec ./myapp -- --flag=v # execute binary with argsCommon Commands
break main.main # set breakpoint
break file.go:42 # break at line
continue # continue execution
next # step over (n)
step # step into (s)
stepout # step out
print variable # print variable
locals # print all locals
args # print function arguments
goroutines # list all goroutines
goroutine 5 # switch to goroutine 5
stack # show stack traceIDE Integration
VS Code:
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"env": { "GOTRACEBACK": "all" }
}
]
}GoLand: Run -> Edit Configurations -> Go Build. Click gutter to set breakpoints. Use Debugger tab.
---
Advanced Analysis
→ See samber/cc-skills-golang@golang-benchmark skill (compiler-analysis.md) for detailed guides on escape analysis interpretation, assembly inspection, and compiler diagnostics (SSA dump, inlining decisions). See also trace.md for execution tracer analysis.
General Debugging Methodology
For any bug, follow this systematic process:
Step 1: Understand Expected vs Actual
Before touching code, articulate clearly:
- What should happen?
- What actually happens?
- What changed recently?
# What changed recently?
git log --oneline -20
git diff HEAD~5
# Binary search for the breaking commit
git bisect start
git bisect bad # current commit is broken
git bisect good abc123 # this commit was working
# git bisect will walk you to the breaking commitStep 2: Get the Full Error
# Full build errors
go build ./... 2>&1
# Verbose test output
go test ./... -v 2>&1
# Static analysis
go vet ./...
# Run linters — see the golang-lint skill for configuration
golangci-lint run ./...Run golangci-lint early in your debugging workflow. It catches unchecked errors, suspicious constructs, and many other issues that are easy to miss by reading code. See the samber/cc-skills-golang@golang-lint skill for configuration and usage.
Step 3: Isolate the Problem
Narrow the scope before investigating deeper:
# Does a single test fail?
go test -run TestSpecificName -v ./pkg/...
# Does it fail without cache?
go test -count=1 -run TestSpecificName ./pkg/...
# Is it a specific package?
go build ./pkg/suspect/...
# Is it flaky? Run multiple times
go test -count=10 -run TestSuspect ./pkg/...Write more tests if you suspect missing test cases or need to test something in different conditions.
Step 4: Check External Dependencies
Sometimes the bug is not in your code. Before diving deeper, verify that external components behave as expected:
# Reproduce an API call outside your app
curl -v -X POST https://api.example.com/endpoint \
-H "Content-Type: application/json" \
-d '{"key": "value"}'
# Check database content directly
psql -h localhost -U myuser -d mydb -c "SELECT * FROM orders WHERE id = 123"
# Or: mysql, mongosh, redis-cli, etc.
# Or use a database MCP server to query interactively
# Test connectivity and DNS resolution
dig api.example.com
nc -zv api.example.com 443
# Check if an external service is responding at all
curl -o /dev/null -s -w "HTTP %{http_code} in %{time_total}s\n" https://api.example.com/health
# Inspect message queue state
rabbitmqctl list_queues
# Or: kafka-console-consumer, redis-cli LLEN, etc.
# Check certificate validity
openssl s_client -connect api.example.com:443 -brief
# Verify environment variables and config
env | grep DATABASE
env | grep API_KEYCommon external causes:
- API contract changed (new required field, different response shape, deprecated endpoint)
- Database schema drift (missing column, changed type, new constraint, migration not applied)
- Expired or rotated credentials, tokens, or certificates
- DNS resolution failure or stale DNS cache
- Rate limiting or quota exhaustion
- External service degraded (slow responses, partial failures, 5xx errors)
- Message queue full, consumer lag, or rebalancing
- Different behavior between environments (staging vs production config, feature flags)
- Clock skew affecting JWT validation, cache TTLs, or scheduled jobs
- TLS/mTLS misconfiguration or CA bundle mismatch
- Network policy or firewall rule change blocking traffic
- Proxy or load balancer misconfiguration (wrong backend, sticky sessions, health check)
- Disk full or read-only filesystem
- File permissions changed
- OOM killer terminated a dependency (database, cache, sidecar)
- Docker/K8s: wrong image tag, missing env var, resource limits, liveness probe misconfigured
- Third-party SDK or library upgrade with breaking behavioral change
- Locale, timezone, or encoding mismatch between systems
- Connection pool exhaustion (database, HTTP, gRPC)
- Upstream returning cached/stale data
- Network issue. Webhook or callback URL changed or unreachable
Step 5: Check Observability Tools
Production debugging MUST start with observability data. The project may already use observability tools that have the answer — look for imports or dependencies like prometheus, opentelemetry, datadog, sentry, elastic/apm in the codebase. Even if you don't see them in code, the developer may have them deployed separately.
If the information is missing, ask the user what monitoring and observability tools they use. Common stacks:
- Prometheus + Grafana — Dashboards may show error rate spikes, latency changes, resource saturation. Query examples:
rate(http_requests_total{status=~"5.."}[5m]) # error rate
histogram_quantile(0.99, rate(http_duration_seconds_bucket[5m])) # p99 latency
go_goroutines # goroutine count over time
go_memstats_alloc_bytes # heap allocations
rate(go_gc_duration_seconds_sum[5m]) # GC pressure- Datadog — APM traces, error tracking, and infrastructure metrics are available. Query examples:
avg:trace.http.request.duration{service:myapp} by {resource_name}
sum:trace.http.request.errors{service:myapp}.as_count()
avg:runtime.go.num_goroutine{service:myapp}- Sentry — Captured exceptions, breadcrumbs, and error grouping are available. Sentry often captures the full stack trace and context of the first occurrence.
- ELK (Elasticsearch + Logstash + Kibana) — Structured logs can be searched for error patterns:
level:error AND service:myapp AND @timestamp:[now-1h TO now]- OpenTelemetry / Jaeger / Zipkin — Distributed traces show latency breakdowns across services, failed spans, and propagation issues.
If the user has an MCP server for any of these tools (Datadog MCP, Grafana MCP, etc.), interactive queries may be available through it.
Step 6: Compare with Working Code
Before forming a hypothesis, find similar code that works:
- Search the codebase for analogous functionality that doesn't have the bug
- Read the working reference implementation completely — don't skim
- List every difference between the working code and the broken code
- Check: are the dependencies the same? The config? The initialization order? The error handling?
Often the bug becomes obvious when you see what the working version does differently.
Step 7: Form a Hypothesis and Test It
- Form a single, specific hypothesis with clear reasoning
- Add targeted logging or a focused test
- Change one thing, observe, confirm or reject
- If the hypothesis was wrong, revert the change — don't stack fixes on top of failed attempts
Step 8: Trace to Root Cause
When the symptom appears deep in the call stack, don't fix where the error surfaces. Trace backward:
1. Find the immediate cause — what line panics or returns the wrong value? 2. Ask "what called this?" — trace one level up the call chain 3. Keep tracing — repeat until you find where the invalid data originated, not where it was consumed 4. Fix at the source — the fix belongs where the bad value was created, not where it caused a crash
// Example: panic in handler — but the bug is in the constructor
// ✗ Bad — fixing at the symptom
func (s *Server) Handle(w http.ResponseWriter, r *http.Request) {
if s.db == nil { // nil check masks the real bug
http.Error(w, "db unavailable", 500)
return
}
// ...
}
// ✓ Good — fixing at the source
func NewServer(db *sql.DB) *Server {
if db == nil {
panic("NewServer: db must not be nil") // fail fast at construction
}
return &Server{db: db}
}When you can't trace manually, add temporary instrumentation:
// Log the full call chain before the dangerous operation
func suspectFunction(val string) {
fmt.Fprintf(os.Stderr, "DEBUG suspectFunction: val=%q\n%s\n", val, debug.Stack())
// ...
}Step 9: Fix and Verify
- Fix the root cause, not the symptom
- The failing test from step 1 should now pass
- Run the full test suite to check for regressions
Step 10: Defense-in-Depth
After fixing a bug, ask: "How do I make this bug structurally impossible?" A single fix at one layer can be bypassed by different code paths or future refactoring. Add validation at multiple layers:
1. Entry point — reject invalid input at public API boundaries (New* constructors, exported functions) 2. Business logic — assert preconditions inside internal functions that receive the data 3. Runtime guards — use build tags or env checks to catch dangerous operations in tests (e.g., refuse writes outside temp dirs) 4. Observability — add structured logging or metrics so the same class of bug is instantly visible if it recurs
Not every fix needs all four layers — use judgment. But when a bug could cause data loss, corruption, or security issues, multi-layer defense is worth the cost.
When You're Stuck: Escalation Protocol
If your fix doesn't work:
- < 3 failed attempts: Return to Step 1. You misidentified the root cause. Gather more evidence.
- >= 3 failed attempts: Stop fixing. The problem is likely architectural, not a simple bug. Step back and question your assumptions about how the system works. Ask: "Is the design fundamentally sound, or am I patching a broken abstraction?"
- Each fix reveals a new problem: You're chasing symptoms, not the root cause. See the Red Flags section in SKILL.md.
Performance Troubleshooting
CPU Profiling
Use pprof CPU profile to capture a 30s sample (see pprof.md for commands), then inspect with top, web, or list funcName.
Common CPU hogs:
1. JSON marshal/unmarshal in hot path — preallocate buffers, use faster libraries 2. Reflection in critical path 3. Unnecessary allocations — use sync.Pool 4. O(n^2) hidden in nested loops 5. Too many syscalls — batch operations
Memory Profiling
Use pprof heap profile (see pprof.md). Compare heap snapshots over time with go tool pprof -base heap1.prof heap2.prof to find growth. Use escape analysis (see diagnostic-tools.md) to find unexpected heap allocations in hot paths.
Common memory leaks:
1. Unbounded cache without eviction 2. Growing slices in loops (forgetting to reset) 3. Global maps never cleared 4. String concatenation in loops (use strings.Builder) 5. Large structs passed by value
Lock Contention
Symptoms: CPU high but throughput low, latency increases with load, multiple cores don't help.
Enable profiling in code:
runtime.SetMutexProfileFraction(1)
runtime.SetBlockProfileRate(1)Then use pprof mutex and block profiles (see pprof.md).
Solutions:
1. Reduce critical section — hold lock for minimal time 2. Sharding — multiple locks for different data 3. sync.Map — for read-heavy workloads 4. atomic — for simple counters 5. RWMutex — when reads >> writes
pprof Reference
Enable pprof HTTP Server
Pprof endpoints MUST be protected with basic auth — NEVER expose them publicly. They leak sensitive runtime information (goroutine stacks, memory contents) and can be abused to DoS your service (CPU profiling is expensive). Pprof SHOULD be toggled via a PPROF_ENABLED environment variable.
Quick Setup (Development)
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ... rest of app
}Secure Setup (Production)
For production, protect endpoints with basic auth:
import "net/http/pprof"
func setupPprof(mux *http.ServeMux) {
if os.Getenv("PPROF_ENABLED") != "true" {
return
}
// Protect pprof endpoints with basic auth — never expose unauthenticated
username := os.Getenv("PPROF_USERNAME")
password := os.Getenv("PPROF_PASSWORD")
if username == "" || password == "" {
panic("PPROF_USERNAME and PPROF_PASSWORD must be set when pprof is enabled")
}
auth := basicAuth(username, password)
mux.Handle("/debug/pprof/", auth(http.HandlerFunc(pprof.Index)))
mux.Handle("/debug/pprof/cmdline", auth(http.HandlerFunc(pprof.Cmdline)))
mux.Handle("/debug/pprof/profile", auth(http.HandlerFunc(pprof.Profile)))
mux.Handle("/debug/pprof/symbol", auth(http.HandlerFunc(pprof.Symbol)))
mux.Handle("/debug/pprof/trace", auth(http.HandlerFunc(pprof.Trace)))
slog.Info("pprof endpoints enabled (basic auth required)")
}
// basicAuth wraps an http.Handler with HTTP Basic Authentication.
func basicAuth(username, password string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, p, ok := r.BasicAuth()
if !ok || u != username || subtle.ConstantTimeCompare([]byte(p), []byte(password)) != 1 {
w.Header().Set("WWW-Authenticate", `Basic realm="pprof"`)
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
}Profile Types
| Profile | Command | What It Shows |
|---|---|---|
| CPU | go tool pprof profile | Where CPU time is spent |
| Heap | go tool pprof heap | Memory allocations, live objects |
| Goroutine | go tool pprof goroutine | Stack traces of all goroutines |
| Block | go tool pprof block | Blocking operations (needs SetBlockProfileRate) |
| Mutex | go tool pprof mutex | Lock contention (needs SetMutexProfileFraction) |
| Alloc | go tool pprof -alloc_space heap | Cumulative allocations (not current heap) |
Capturing Profiles
# CPU profiles SHOULD capture at least 30 seconds for meaningful data (30s default).
# Ensure your HTTP server's request timeout exceeds the capture duration.
curl http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.prof
# Heap snapshot
curl http://localhost:6060/debug/pprof/heap > heap.prof
# Goroutine dump (human-readable)
curl http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutines.txt
# Goroutine profile (for pprof analysis)
curl http://localhost:6060/debug/pprof/goroutine > goroutine.prof
# Go 1.26 experimental goroutine leak profile, only with GOEXPERIMENT=goroutineleakprofile
curl http://localhost:6060/debug/pprof/goroutineleak?debug=2
go tool pprof http://localhost:6060/debug/pprof/goroutineleak
# Mutex contention
curl http://localhost:6060/debug/pprof/mutex > mutex.prof
# Block profile
curl http://localhost:6060/debug/pprof/block > block.profAnalyzing and Interpreting Profiles
→ See samber/cc-skills-golang@golang-benchmark skill (pprof.md) for interpreting profiles: top, list, peek, common profile patterns (flat vs cum, GC churn, memory leaks), and compiler diagnostics. See also compiler-analysis.md for escape analysis and inlining decisions.
Quick start:
go tool pprof cpu.prof # interactive analysis
go tool pprof -http=:8080 cpu.prof # graphical flamegraph
go tool pprof -base heap1.prof heap2.prof # compare heap snapshotsRemote Profiling (Production)
For production servers, replace localhost:6060 with your server address and use basic auth credentials.
Safety: idle pprof endpoints have low overhead, but profile captures are not free. CPU profiling samples for the requested duration, heap profiles may trigger extra work, and block/mutex profiles add runtime overhead when enabled.
---
→ See samber/cc-skills-golang@golang-observability skill for continuous profiling with Pyroscope. → See samber/cc-skills-golang@golang-benchmark skill for investigation session setup and Prometheus-based performance tracking.
Production Debugging
Production Debugging Checklist
When paged for a production issue:
Step 1: Capture Immediately (don't restart!)
Capture all profiles before restarting the process. The curl commands in pprof.md can be used targeting your production server address. At minimum, capture: goroutine dump (?debug=2), heap, CPU (30s), and mutex profiles.
Step 2: System Metrics
ps aux | grep myapp
lsof -p PID | wc -l # file descriptors
ss -s # socket summary
netstat -an | grep ESTABLISHED | wc -lStep 3: Analyze Locally
Download the captured .prof files and analyze with go tool pprof (see pprof.md).
---
Logging & Observability
Strategic Log Placement
Place logs at component boundaries, not sprinkled randomly. The goal is to see data entering and exiting each layer, so you can identify exactly which component corrupts or drops it:
// 1. Function entry/exit with key parameters
func ProcessOrder(ctx context.Context, orderID string) error {
log.Printf("ProcessOrder: start orderID=%s", orderID)
defer log.Printf("ProcessOrder: done orderID=%s", orderID)
// ...
}
// 2. Before and after external calls
log.Printf("calling payment API for order %s", orderID)
resp, err := paymentClient.Charge(ctx, req)
if err != nil {
log.Printf("payment API: err=%v", err)
} else {
log.Printf("payment API: status=%d", resp.StatusCode)
}
// 3. At decision points
if user.IsAdmin {
log.Printf("admin path for user %s", user.ID)
}Structured Logging (Go 1.21+)
import "log/slog"
slog.Info("processing request",
"method", r.Method,
"path", r.URL.Path,
"user_id", userID,
)
slog.Error("database query failed",
"err", err,
"query", query,
"duration_ms", elapsed.Milliseconds(),
)Request ID Tracing
type ctxKey string
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, ctxKey("request_id"), id)
}
func RequestID(ctx context.Context) string {
id, _ := ctx.Value(ctxKey("request_id")).(string)
return id
}---
Network & HTTP Debugging
HTTP Client Issues
// 1. HTTP clients MUST set timeouts — default http.Client has NO timeout
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
IdleConnTimeout: 90 * time.Second,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
},
}
// 2. Response body MUST be closed
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// 3. Read body on error status (for error messages from server)
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API error %d: %s", resp.StatusCode, body)
}
// 4. Dump full request/response for debugging
import "net/http/httputil"
dump, _ := httputil.DumpRequestOut(req, true)
log.Printf("request:\n%s", dump)
dump, _ = httputil.DumpResponse(resp, true)
log.Printf("response:\n%s", dump)Test-Driven Debugging
A failing test MUST be written before fixing a bug. Writing a failing test is often the fastest debugging path. It gives you a reproducible, isolated environment.
Reproduce the Bug in a Test
func TestBugDescription(t *testing.T) {
// Setup: exact conditions that trigger the bug
svc := NewService(testConfig)
// Act: the operation that fails
result, err := svc.Process(badInput)
// Assert: what should happen
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Status != "ok" {
t.Errorf("got status %q, want %q", result.Status, "ok")
}
}Expand Edge Cases with Table Tests
When debugging, add edge cases to find the boundary of the bug:
tests := []struct {
name string
input string
want time.Duration
wantErr bool
}{
{"valid", "5s", 5 * time.Second, false},
{"empty", "", 0, true},
{"negative", "-1s", -time.Second, false},
{"zero", "0s", 0, false},
{"overflow", "99999999h", 0, true},
{"whitespace", " 5s ", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseDuration(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}Useful Test Flags
go test -v ./... # verbose output
go test -run TestName -v ./pkg/... # single test
go test -count=1 ./... # disable cache
go test -timeout 10s ./... # short timeout (find hangs)
go test -parallel 1 ./... # sequential execution
go test -race ./... # race detector
go test -cover ./... # coverage summary
go test -coverprofile=c.out ./... && go tool cover -html=c.out # coverage report
go test -failfast ./... # stop on first failure
go test -shuffle=on ./... # randomize test order (Go 1.17+)Debugging Flaky Tests
Flaky tests (pass sometimes, fail sometimes) are usually caused by one of:
1. Shared mutable state between tests — global variables, package-level maps, singletons
- Fix: reset state in
TestMainor uset.Cleanup
2. Test order dependence — one test sets up state another test relies on
- Diagnose:
go test -run TestSuspect -count=1(run in isolation) - Diagnose:
go test -shuffle=on(randomize order) - Fix: each test must set up its own preconditions
3. Timing sensitivity — time.Sleep in tests, race between goroutines
- Fix: use channels/waitgroups to synchronize, not sleeps
4. Port conflicts — tests binding to fixed ports
- Fix: use port
0and read the assigned port
5. File system pollution — tests writing to shared temp directories
- Fix: use
t.TempDir()for per-test directories
# Confirm flakiness by running many times
go test -count=100 -run TestSuspect ./pkg/... -failfast
# Check for parallelism issues
go test -parallel 1 -count=10 ./pkg/...
# Check for order dependence
go test -shuffle=on ./pkg/...Related skills
How it compares
Use golang-troubleshooting for local bug reproduction; use golang-observability when production logs, metrics, or traces drive the investigation.
FAQ
What is the core rule of golang-troubleshooting?
No fixes without root-cause investigation first: reproduce before you fix, take one hypothesis at a time, and never propose a fix you cannot explain.
What tools does it use?
It escalates incrementally from fmt.Println and test isolation to pprof, the Delve debugger (dlv), go test -race, and GODEBUG tracing.
Is Golang Troubleshooting safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.