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 version

Basic 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
exit

Launch Configuration

# Debug existing binary
dlv exec ./myprogram

# Attach to running process
dlv attach <pid>

# Debug test
dlv test

Advanced 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 > 10

Watchpoints

Watch for changes to variables.

# Set watchpoint on variable
watch -w sum

# Continue and see when sum changes
continue

Goroutine Debugging

Debug concurrent programs.

# List all goroutines
goroutines

# Switch to specific goroutine
goroutine 3

# Check goroutine stack
stack

Runtime 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 visualization

Race 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 -race

Example 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 RACE

Debugging 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_binary

Flame 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:8080

Debugging 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:

Last updated on