Go Debugging Guide
Go Debugging Guide
Section titled “Go Debugging Guide”A comprehensive guide to debugging Go programs, covering both Delve (recommended) and GDB.
Table of Contents
Section titled “Table of Contents”- Delve - The Go Debugger (Recommended)
- GDB - GNU Debugger (Alternative)
- Print Debugging
- Common Debugging Scenarios
Delve - The Go Debugger (Recommended)
Section titled “Delve - The Go Debugger (Recommended)”Delve (dlv) is the recommended debugger for Go programs. It’s specifically designed for Go and provides excellent support for:
- Goroutines
- Go runtime internals
- Go-specific data types
- Concurrent code debugging
Installation
Section titled “Installation”macOS:
brew install delveLinux:
go install github.com/go-delve/delve/cmd/dlv@latestWindows:
go install github.com/go-delve/delve/cmd/dlv@latestVerify installation:
dlv versionBasic Usage
Section titled “Basic Usage”Debug a Go Program
Section titled “Debug a Go Program”Option 1: Debug directly
dlv debug main.goOption 2: Debug with arguments
dlv debug main.go -- arg1 arg2Option 3: Attach to running process
# Get process IDps aux | grep myprogram
# Attach to processdlv attach <PID>Option 4: Debug a test
dlv test ./path/to/packageOption 5: Debug a compiled binary
# Compile with debugging infogo build -gcflags="all=-N -l" -o myapp main.go
# Debug the binarydlv exec ./myappBreakpoints
Section titled “Breakpoints”Set Breakpoints
Section titled “Set Breakpoints”# Start debuggerdlv debug main.go
# Inside dlv prompt:(dlv) break main.mainBreakpoint 1 set at 0x... for main.main() ./main.go:10
# Break at specific line(dlv) break main.go:15Breakpoint 2 set at 0x... for main.processData() ./main.go:15
# Break at function in package(dlv) break fmt.PrintlnBreakpoint 3 set at 0x... for fmt.Println()
# List all breakpoints(dlv) breakpointsBreakpoint 1 at 0x... for main.main() ./main.go:10Breakpoint 2 at 0x... for main.processData() ./main.go:15
# Clear breakpoint(dlv) clear 2
# Clear all breakpoints(dlv) clearallStepping Through Code
Section titled “Stepping Through Code”Example program to debug:
package main
import "fmt"
func calculate(x, y int) int { result := x + y return result * 2}
func main() { a := 5 b := 10 sum := calculate(a, b) fmt.Println("Result:", sum)}Debugging session:
$ dlv debug main.goType 'help' for list of commands.
# Set breakpoint at main(dlv) break main.mainBreakpoint 1 set at 0x10a4780 for main.main() ./main.go:11
# Run program(dlv) continue> main.main() ./main.go:11 (hits breakpoint[1])
# Show current code(dlv) list> 11: func main() { 12: a := 5 13: b := 10 14: sum := calculate(a, b) 15: fmt.Println("Result:", sum) 16: }
# Step to next line (stays in current function)(dlv) next> main.main() ./main.go:12
(dlv) next> main.main() ./main.go:13
# Print variable value(dlv) print a5
# Step into function call(dlv) step> main.calculate() ./main.go:6
# Show where we are(dlv) list> 6: func calculate(x, y int) int { 7: result := x + y 8: return result * 2 9: }
# Print function arguments(dlv) argsx = 5y = 10
# Continue to next line(dlv) next> main.calculate() ./main.go:7
# Print local variables(dlv) localsresult = 842288255632 (uninitialized)
# Execute next line(dlv) next> main.calculate() ./main.go:8
# Check result after calculation(dlv) print result15
# Step out of current function(dlv) stepout> main.main() ./main.go:14
# Print return value(dlv) print sum30
# Continue to end(dlv) continueResult: 30Process exited with status 0Inspecting Variables
Section titled “Inspecting Variables”# Print variable(dlv) print myVar
# Print with full type information(dlv) print -v myVar
# Print pointer dereference(dlv) print *myPointer
# Print struct fields(dlv) print myStruct.fieldName
# Print slice/array elements(dlv) print mySlice[0]
# Print all local variables(dlv) locals
# Print all function arguments(dlv) args
# Print all variables (locals + args)(dlv) vars
# Set variable value(dlv) set myVar = 42
# Call function(dlv) call myFunction(arg1, arg2)Goroutine Debugging
Section titled “Goroutine Debugging”Example with goroutines:
package main
import ( "fmt" "time")
func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Printf("worker %d processing job %d\n", id, j) time.Sleep(time.Second) results <- j * 2 }}
func main() { jobs := make(chan int, 5) results := make(chan int, 5)
// Start 3 workers for w := 1; w <= 3; w++ { go worker(w, jobs, results) }
// Send jobs for j := 1; j <= 5; j++ { jobs <- j } close(jobs)
// Collect results for a := 1; a <= 5; a++ { <-results }}Debug goroutines:
$ dlv debug concurrent.go
# Set breakpoint in worker function(dlv) break workerBreakpoint 1 set at 0x... for main.worker() ./concurrent.go:9
# Run(dlv) continue
# List all goroutines(dlv) goroutines* Goroutine 1 - User: ./concurrent.go:26 main.main (0x10a4980) Goroutine 2 - User: ./concurrent.go:10 main.worker (0x10a4820) Goroutine 3 - User: ./concurrent.go:10 main.worker (0x10a4820) Goroutine 4 - User: ./concurrent.go:10 main.worker (0x10a4820)
# Switch to specific goroutine(dlv) goroutine 2Switched to goroutine 2
# Print goroutine-local variables(dlv) localsid = 1j = 1
# View stack trace for current goroutine(dlv) stack
# View stack trace for all goroutines(dlv) goroutines -t
# Continue all goroutines(dlv) continueConditional Breakpoints
Section titled “Conditional Breakpoints”# Break only when condition is true(dlv) break main.go:15(dlv) condition 1 myVar > 100
# Break with hit count(dlv) break main.go:20(dlv) condition 2 count == 5
# Clear condition(dlv) condition 1 -clear
# Break with complex condition(dlv) break processData(dlv) condition 3 len(data) > 0 && data[0] == "test"Advanced Delve Commands
Section titled “Advanced Delve Commands”# Show source around current line(dlv) list(dlv) ls # short form(dlv) list 50 # show line 50
# Show stack trace(dlv) stack(dlv) bt # short form
# Move up/down stack frames(dlv) up(dlv) down(dlv) frame 2 # jump to specific frame
# Restart program(dlv) restart(dlv) r # short form
# Exit debugger(dlv) exit(dlv) quit(dlv) q # short form
# Help(dlv) help(dlv) help break # help for specific commandVS Code Integration
Section titled “VS Code Integration”Install Go extension:
- Open VS Code
- Install “Go” extension by Go Team at Google
Debug configuration (.vscode/launch.json):
{ "version": "0.2.0", "configurations": [ { "name": "Launch Package", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceFolder}", "args": [], "showLog": true }, { "name": "Launch File", "type": "go", "request": "launch", "mode": "debug", "program": "${file}" }, { "name": "Attach to Process", "type": "go", "request": "attach", "mode": "local", "processId": 0 }, { "name": "Debug Test", "type": "go", "request": "launch", "mode": "test", "program": "${workspaceFolder}" } ]}Using VS Code debugger:
- Set breakpoints by clicking left of line numbers
- Press
F5to start debugging - Use debug toolbar:
- Continue (F5)
- Step Over (F10)
- Step Into (F11)
- Step Out (Shift+F11)
- Restart (Ctrl+Shift+F5)
- Stop (Shift+F5)
- Inspect variables in “Variables” panel
- View call stack in “Call Stack” panel
- Watch expressions in “Watch” panel
GDB - GNU Debugger (Alternative)
Section titled “GDB - GNU Debugger (Alternative)”⚠️ Warning: GDB support for Go exists but is limited. Delve is strongly recommended for Go debugging. GDB can be difficult to set up, especially on macOS, and lacks proper support for:
- Goroutines
- Go-specific data types
- Go runtime internals
Use GDB only if Delve is unavailable.
Setup and Compilation
Section titled “Setup and Compilation”Step 1: Compile with Debug Symbols
Section titled “Step 1: Compile with Debug Symbols”# Disable optimization and inlininggo build -gcflags="-N -l" -o myapp main.goFlags explained:
-N: Disable optimizations-l: Disable inlining
Step 2: Launch GDB
Section titled “Step 2: Launch GDB”gdb myappStep 3: Load Go Runtime Support (Optional)
Section titled “Step 3: Load Go Runtime Support (Optional)”Inside GDB:
(gdb) source $GOROOT/src/runtime/runtime-gdb.pyIf this fails, find the correct path:
# Find Go installationgo env GOROOT
# Use full path(gdb) source /usr/local/go/src/runtime/runtime-gdb.pyOn macOS with Homebrew Go:
(gdb) source /usr/local/opt/go/libexec/src/runtime/runtime-gdb.pyBasic GDB Commands
Section titled “Basic GDB Commands”Example debugging session:
$ gdb myappGNU gdb (GDB) 12.1...
# Set breakpoint(gdb) break main.mainBreakpoint 1 at 0x...
# Set breakpoint at line number(gdb) break main.go:22Breakpoint 2 at 0x...
# Run program(gdb) run
# Run with arguments(gdb) run arg1 arg2
# List source code(gdb) list
# Print variable(gdb) print myVar
# Print with type info(gdb) whatis myVar
# Step to next line (skip over functions)(gdb) next(gdb) n # short form
# Step into function(gdb) step(gdb) s # short form
# Continue execution(gdb) continue(gdb) c # short form
# View local variables(gdb) info locals
# View stack trace(gdb) backtrace(gdb) bt # short form
# Quit GDB(gdb) quit(gdb) q # short formGDB Command Reference
Section titled “GDB Command Reference”| Command | Short | Description |
|---|---|---|
run | r | Start program execution |
break <location> | b | Set breakpoint |
continue | c | Resume execution |
next | n | Step over (next line) |
step | s | Step into function |
finish | fin | Step out of function |
list | l | Show source code |
print <var> | p | Print variable value |
info locals | Show local variables | |
info args | Show function arguments | |
backtrace | bt | Show call stack |
frame <n> | f | Select stack frame |
delete <n> | d | Delete breakpoint |
quit | q | Exit GDB |
Limitations of GDB with Go
Section titled “Limitations of GDB with Go”- Poor goroutine support: Cannot easily inspect or switch between goroutines
- Go runtime complexity: GDB struggles with Go’s runtime internals
- Type system: Limited understanding of Go-specific types (channels, interfaces, etc.)
- macOS issues: Requires code signing and special setup on macOS
- Optimization: Go compiler optimizations can confuse GDB
Recommendation: Use Delve instead of GDB for Go programs.
Print Debugging
Section titled “Print Debugging”Sometimes simple print statements are the fastest way to debug.
Using fmt.Println
Section titled “Using fmt.Println”package main
import "fmt"
func calculate(x, y int) int { fmt.Printf("DEBUG: calculate called with x=%d, y=%d\n", x, y) result := x + y fmt.Printf("DEBUG: result before multiply=%d\n", result) result = result * 2 fmt.Printf("DEBUG: final result=%d\n", result) return result}
func main() { sum := calculate(5, 10) fmt.Println("Result:", sum)}Using log Package
Section titled “Using log Package”package main
import ( "log")
func init() { // Add file and line numbers to log output log.SetFlags(log.Lshortfile | log.LstdFlags)}
func processData(data []string) { log.Printf("Processing %d items", len(data)) for i, item := range data { log.Printf("Item %d: %s", i, item) }}Conditional Debug Output
Section titled “Conditional Debug Output”package main
import ( "fmt" "os")
var debug = os.Getenv("DEBUG") == "1"
func debugf(format string, args ...interface{}) { if debug { fmt.Printf("[DEBUG] "+format+"\n", args...) }}
func main() { debugf("Starting application") // Regular code debugf("Processing complete")}Usage:
# Run without debuggo run main.go
# Run with debug outputDEBUG=1 go run main.goCommon Debugging Scenarios
Section titled “Common Debugging Scenarios”Debugging a Panic
Section titled “Debugging a Panic”package main
func divide(a, b int) int { return a / b}
func main() { result := divide(10, 0) // Will panic println(result)}Debug with Delve:
$ dlv debug panic.go
(dlv) break main.main(dlv) continue
# Step through to see where panic occurs(dlv) next> panic: runtime error: integer divide by zeroDebugging Race Conditions
Section titled “Debugging Race Conditions”package main
import ( "fmt" "time")
func main() { counter := 0
// Multiple goroutines modifying same variable for i := 0; i < 100; i++ { go func() { counter++ // Race condition! }() }
time.Sleep(time.Second) fmt.Println("Counter:", counter)}Detect with race detector:
# Run with race detectorgo run -race race.go
# Output shows race condition:# WARNING: DATA RACE# Write at 0x... by goroutine 7:# main.main.func1()# race.go:13 +0x...Fix with mutex:
import ( "fmt" "sync" "time")
func main() { var mu sync.Mutex counter := 0
for i := 0; i < 100; i++ { go func() { mu.Lock() counter++ mu.Unlock() }() }
time.Sleep(time.Second) fmt.Println("Counter:", counter)}Debugging Deadlocks
Section titled “Debugging Deadlocks”package main
func main() { ch := make(chan int)
// Trying to receive from empty unbuffered channel val := <-ch // Deadlock! No one sends println(val)}Run:
$ go run deadlock.gofatal error: all goroutines are asleep - deadlock!Debug with Delve:
$ dlv debug deadlock.go
(dlv) break main.main(dlv) continue(dlv) next# Shows program hanging on channel receive
# Check goroutines(dlv) goroutines* Goroutine 1 - User: ./deadlock.go:6 main.main (0x...) [waiting for channel receive]Debugging HTTP Handlers
Section titled “Debugging HTTP Handlers”package main
import ( "fmt" "net/http")
func handler(w http.ResponseWriter, r *http.Request) { // Set breakpoint here to inspect requests fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])}
func main() { http.HandleFunc("/", handler) http.ListenAndServe(":8080", nil)}Debug with Delve:
$ dlv debug server.go
(dlv) break handler(dlv) continue
# In another terminal, make request:# curl localhost:8080/world
# Back in debugger:> main.handler() ./server.go:9
(dlv) print r.URL.Path"/world"
(dlv) print r.Method"GET"
(dlv) continueQuick Reference Card
Section titled “Quick Reference Card”Delve Commands
Section titled “Delve Commands”| Command | Action |
|---|---|
dlv debug | Start debugging |
break <location> | Set breakpoint |
continue / c | Continue execution |
next / n | Step over |
step / s | Step into |
stepout | Step out |
print <var> / p | Print variable |
locals | Show local variables |
args | Show function arguments |
goroutines | List goroutines |
goroutine <id> | Switch to goroutine |
stack / bt | Show stack trace |
list / ls | Show source |
restart / r | Restart program |
exit / quit / q | Exit debugger |
Compilation Flags
Section titled “Compilation Flags”# Disable optimizations for debugginggo build -gcflags="all=-N -l" -o myapp
# Enable race detectorgo build -race -o myapp
# Bothgo build -race -gcflags="all=-N -l" -o myappEnvironment Variables
Section titled “Environment Variables”# Enable debug outputDEBUG=1 go run main.go
# Enable race detectorgo run -race main.go
# Set GOMAXPROCS (for concurrency debugging)GOMAXPROCS=1 go run main.goBest Practices
Section titled “Best Practices”- Use Delve for Go debugging (not GDB)
- Compile with
-gcflags="all=-N -l"for better debugging - Use race detector (
-race) for concurrent code - Set breakpoints strategically at function entry and key logic points
- Inspect goroutines when debugging concurrent code
- Use conditional breakpoints to catch specific cases
- Integrate with your IDE (VS Code, GoLand) for visual debugging
- Keep print debugging as a quick alternative for simple issues
- Enable verbose logging in development
- Test with
-vflag to see detailed test output
Resources
Section titled “Resources”- Delve Documentation: github.com/go-delve/delve
- Delve CLI Commands: github.com/go-delve/delve/tree/master/Documentation/cli
- VS Code Go Debugging: code.visualstudio.com/docs/languages/go
- Go Race Detector: go.dev/doc/articles/race_detector
- Effective Go Debugging: go.dev/doc/effective_go
Master debugging to write better Go code faster!