Debugging Go Programs
Debugging is a crucial skill for any developer. Go provides excellent debugging tools and techniques. This tutorial covers various debugging approaches, from print statements to advanced debugging tools like Delve.
Print Debugging
Basic Print Statements
The simplest debugging technique is adding print statements to your code.
package main
import "fmt"
func calculateSum(numbers []int) int {
fmt.Printf("DEBUG: calculateSum called with %d numbers\n", len(numbers))
sum := 0
for i, num := range numbers {
fmt.Printf("DEBUG: adding number[%d] = %d\n", i, num)
sum += num
}
fmt.Printf("DEBUG: final sum = %d\n", sum)
return sum
}
func main() {
result := calculateSum([]int{1, 2, 3, 4, 5})
fmt.Printf("Result: %d\n", result)
}Structured Logging
Use proper logging instead of fmt.Printf for production code.
package main
import (
"log"
"os"
)
func init() {
// Set up logging with timestamp and file info
log.SetFlags(log.LstdFlags | log.Lshortfile)
// Only enable debug logging in development
if os.Getenv("DEBUG") == "true" {
log.SetOutput(os.Stderr)
} else {
// Discard logs in production
log.SetOutput(io.Discard)
}
}
func calculateSum(numbers []int) int {
log.Printf("calculateSum called with %d numbers", len(numbers))
sum := 0
for i, num := range numbers {
log.Printf("adding number[%d] = %d", i, num)
sum += num
}
log.Printf("final sum = %d", sum)
return sum
}Using Delve (dlv)
Delve is Go’s official debugger, providing powerful debugging capabilities.
Installing Delve
# Install Delve
go install github.com/go-delve/delve/cmd/dlv@latest
# Verify installation
dlv versionBasic Debugging Session
package main
import "fmt"
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
func main() {
result := fibonacci(10)
fmt.Printf("Fibonacci(10) = %d\n", result)
}Debugging commands:
# Start debugging
dlv debug main.go
# Set breakpoint
break main.fibonacci
# Run program
continue
# Step into function
step
# Step over
next
# Print variable
print n
# List source code
list
# Exit
exitLaunch Configuration
# Debug existing binary
dlv exec ./myprogram
# Attach to running process
dlv attach <pid>
# Debug test
dlv testAdvanced Debugging Techniques
Conditional Breakpoints
Set breakpoints that only trigger under certain conditions.
# Break when variable equals specific value
break main.fibonacci n == 5
# Break when condition is true
break main.loop i > 10Watchpoints
Watch for changes to variables.
# Set watchpoint on variable
watch -w sum
# Continue and see when sum changes
continueGoroutine Debugging
Debug concurrent programs.
# List all goroutines
goroutines
# Switch to specific goroutine
goroutine 3
# Check goroutine stack
stackRuntime Debugging
Stack Traces
Get stack traces programmatically.
package main
import (
"fmt"
"runtime"
)
func traceFunction() {
// Print stack trace
buf := make([]byte, 1024)
runtime.Stack(buf, false)
fmt.Printf("Stack trace:\n%s\n", buf)
}
func recursiveFunction(n int) {
if n == 0 {
traceFunction()
return
}
recursiveFunction(n - 1)
}
func main() {
recursiveFunction(5)
}Panic Recovery with Stack Traces
package main
import (
"fmt"
"runtime/debug"
)
func riskyFunction() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: %v\n", r)
fmt.Printf("Stack trace:\n%s\n", debug.Stack())
}
}()
panic("something went wrong")
}
func main() {
riskyFunction()
fmt.Println("Program continues after recovery")
}Profiling and Performance Debugging
CPU Profiling
package main
import (
"os"
"runtime/pprof"
)
func main() {
// Create CPU profile file
f, err := os.Create("cpu.prof")
if err != nil {
panic(err)
}
defer f.Close()
// Start CPU profiling
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Your code here...
}Memory Profiling
package main
import (
"os"
"runtime/pprof"
)
func main() {
// Your code here...
// Write memory profile
f, err := os.Create("mem.prof")
if err != nil {
panic(err)
}
defer f.Close()
runtime.GC() // Force garbage collection
pprof.WriteHeapProfile(f)
}Analyzing Profiles
# Analyze CPU profile
go tool pprof cpu.prof
# Analyze memory profile
go tool pprof mem.prof
# Interactive commands:
# top - show top functions
# list function_name - show source code
# web - generate web visualizationRace Detection
Go has built-in race detection for concurrent programs.
# Run with race detection
go run -race main.go
# Test with race detection
go test -race
# Build with race detection
go build -raceExample of race condition:
package main
import (
"fmt"
"sync"
)
func main() {
var counter int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // Race condition!
}()
}
wg.Wait()
fmt.Printf("Final counter: %d\n", counter)
}Run with race detection:
go run -race main.go
# Output: WARNING: DATA RACEDebugging HTTP Servers
HTTP Debugging Middleware
package main
import (
"log"
"net/http"
"time"
)
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("Started %s %s", r.Method, r.URL.Path)
log.Printf("Headers: %v", r.Header)
next(w, r)
log.Printf("Completed %s %s in %v", r.Method, r.URL.Path, time.Since(start))
}
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Processing request for %s", r.URL.Path)
w.Write([]byte("Hello, World!"))
}
func main() {
http.HandleFunc("/hello", loggingMiddleware(helloHandler))
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}Request/Response Dumping
package main
import (
"io"
"log"
"net/http"
"net/http/httputil"
)
func debugHandler(w http.ResponseWriter, r *http.Request) {
// Dump request
requestDump, err := httputil.DumpRequest(r, true)
if err != nil {
log.Printf("Error dumping request: %v", err)
} else {
log.Printf("Request:\n%s", requestDump)
}
// Process request
w.Write([]byte("Debug response"))
}
func main() {
http.HandleFunc("/debug", debugHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}IDE Debugging
VS Code Configuration
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Go Program",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/main.go"
},
{
"name": "Debug Test",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}",
"args": ["-test.v"]
}
]
}GoLand Debugging
GoLand has built-in debugging support with visual breakpoints, variable inspection, and step-through debugging.
Debugging Best Practices
1. Use Meaningful Variable Names
// Good
userCount := len(users)
totalPrice := calculateTotal(cart)
// Avoid
c := len(u)
tp := calc(cart)2. Add Context to Debug Messages
// Good
log.Printf("Processing user %s (ID: %d) with %d items", user.Name, user.ID, len(user.Items))
// Avoid
log.Printf("Processing: %v", user)3. Use Structured Logging
import "github.com/sirupsen/logrus"
log.WithFields(logrus.Fields{
"user_id": userID,
"action": "login",
"ip": r.RemoteAddr,
}).Info("User login attempt")4. Create Debug Builds
var debug = flag.Bool("debug", false, "enable debug mode")
func debugLog(msg string, args ...interface{}) {
if *debug {
log.Printf(msg, args...)
}
}5. Use Panic for Programming Errors
func mustParseInt(s string) int {
if result, err := strconv.Atoi(s); err != nil {
panic(fmt.Sprintf("failed to parse int %q: %v", s, err))
} else {
return result
}
}Common Debugging Scenarios
1. Infinite Loops
// Problematic code
for i := 0; i < 10; {
if someCondition {
// Forgot to increment i
}
}
// Fixed code
for i := 0; i < 10; i++ {
if someCondition {
// Process
}
}2. Slice Index Out of Bounds
// Problematic code
numbers := []int{1, 2, 3}
fmt.Println(numbers[10]) // Panic!
// Safe access
if index < len(numbers) {
fmt.Println(numbers[index])
} else {
log.Printf("Index %d out of bounds for slice of length %d", index, len(numbers))
}3. Nil Pointer Dereference
// Problematic code
var user *User
fmt.Println(user.Name) // Panic!
// Safe access
if user != nil {
fmt.Println(user.Name)
} else {
log.Println("User is nil")
}4. Race Conditions
// Problematic code
var counter int
go func() { counter++ }()
go func() { counter++ }()
// Fixed code
var counter int
var mu sync.Mutex
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
go func() {
mu.Lock()
counter++
mu.Unlock()
}()Advanced Tools
GDB with Go
While Delve is preferred, you can use GDB for low-level debugging:
# Build with debug info
go build -gcflags="-N -l" -o debug_binary
# Debug with gdb
gdb debug_binaryFlame Graphs
Generate flame graphs for performance analysis:
# Install tools
go install github.com/uber/go-torch@latest
# Generate flame graph
go-torch -u http://localhost:8080Debugging in Production
Safe Debug Logging
type SafeLogger struct {
mu sync.Mutex
log *log.Logger
}
func (sl *SafeLogger) Debug(msg string, args ...interface{}) {
if os.Getenv("DEBUG") != "true" {
return
}
sl.mu.Lock()
defer sl.mu.Unlock()
sl.log.Printf("[DEBUG] "+msg, args...)
}Health Check Endpoints
func healthHandler(w http.ResponseWriter, r *http.Request) {
// Perform health checks
dbOK := checkDatabase()
cacheOK := checkCache()
status := "OK"
if !dbOK || !cacheOK {
status = "DEGRADED"
w.WriteHeader(http.StatusServiceUnavailable)
}
response := map[string]interface{}{
"status": status,
"checks": map[string]bool{
"database": dbOK,
"cache": cacheOK,
},
"timestamp": time.Now(),
}
json.NewEncoder(w).Encode(response)
}External Resources:
Related Tutorials: