Debugging System-Level Issues in Go
Introduction
Debugging system-level issues requires specialized tools and techniques. This guide covers debugging strategies, tools, and common issues.
Effective debugging enables quick identification and resolution of system-level problems.
Debugging Tools
Delve Debugger
package main
import (
"fmt"
)
// DebugExample demonstrates debugging
func DebugExample() {
x := 10
y := 20
// Set breakpoint here: break main.DebugExample
result := x + y
fmt.Printf("Result: %d\n", result)
}
// Usage:
// dlv debug
// (dlv) break main.DebugExample
// (dlv) continue
// (dlv) next
// (dlv) print x
// (dlv) print y
Printf Debugging
package main
import (
"fmt"
"log"
)
// DebugLog provides debug logging
type DebugLog struct {
enabled bool
}
// NewDebugLog creates a new debug logger
func NewDebugLog(enabled bool) *DebugLog {
return &DebugLog{enabled: enabled}
}
// Printf prints debug message
func (dl *DebugLog) Printf(format string, args ...interface{}) {
if dl.enabled {
log.Printf("[DEBUG] "+format, args...)
}
}
// Example usage
func PrintfDebuggingExample() {
debug := NewDebugLog(true)
x := 10
debug.Printf("x = %d\n", x)
y := 20
debug.Printf("y = %d\n", y)
result := x + y
debug.Printf("result = %d\n", result)
}
Good: Proper Debugging Implementation
package main
import (
"fmt"
"log"
"os"
"runtime"
"runtime/debug"
"time"
)
// Debugger provides debugging utilities
type Debugger struct {
enabled bool
logger *log.Logger
}
// NewDebugger creates a new debugger
func NewDebugger(enabled bool) *Debugger {
return &Debugger{
enabled: enabled,
logger: log.New(os.Stderr, "[DEBUG] ", log.LstdFlags),
}
}
// Log logs a message
func (d *Debugger) Log(msg string) {
if d.enabled {
d.logger.Println(msg)
}
}
// Logf logs a formatted message
func (d *Debugger) Logf(format string, args ...interface{}) {
if d.enabled {
d.logger.Printf(format, args...)
}
}
// PrintStack prints stack trace
func (d *Debugger) PrintStack() {
if d.enabled {
debug.PrintStack()
}
}
// PrintMemStats prints memory statistics
func (d *Debugger) PrintMemStats() {
if d.enabled {
var m runtime.MemStats
runtime.ReadMemStats(&m)
d.Logf("Alloc: %v MB\n", m.Alloc/1024/1024)
d.Logf("TotalAlloc: %v MB\n", m.TotalAlloc/1024/1024)
d.Logf("Sys: %v MB\n", m.Sys/1024/1024)
d.Logf("NumGC: %v\n", m.NumGC)
}
}
// PrintGoroutines prints goroutine information
func (d *Debugger) PrintGoroutines() {
if d.enabled {
d.Logf("Goroutines: %d\n", runtime.NumGoroutine())
}
}
// Trace traces function execution
func (d *Debugger) Trace(name string) func() {
if !d.enabled {
return func() {}
}
start := time.Now()
d.Logf(">>> %s\n", name)
return func() {
d.Logf("<<< %s (%v)\n", name, time.Since(start))
}
}
// AssertEqual asserts equality
func (d *Debugger) AssertEqual(expected, actual interface{}, msg string) {
if d.enabled && expected != actual {
d.Logf("ASSERTION FAILED: %s (expected %v, got %v)\n", msg, expected, actual)
}
}
// AssertTrue asserts condition
func (d *Debugger) AssertTrue(condition bool, msg string) {
if d.enabled && !condition {
d.Logf("ASSERTION FAILED: %s\n", msg)
}
}
// Example usage
func DebuggingExample() {
debugger := NewDebugger(true)
defer debugger.Trace("DebuggingExample")()
x := 10
debugger.Logf("x = %d\n", x)
y := 20
debugger.Logf("y = %d\n", y)
result := x + y
debugger.AssertEqual(30, result, "x + y should equal 30")
debugger.PrintMemStats()
debugger.PrintGoroutines()
}
Bad: Improper Debugging
package main
// BAD: No debugging output
func BadNoDebugging() {
x := 10
y := 20
result := x + y
// No way to debug
}
// BAD: Hardcoded debug output
func BadHardcodedDebug() {
fmt.Println("DEBUG: x = 10")
fmt.Println("DEBUG: y = 20")
// Can't disable
}
// BAD: No error context
func BadNoContext() {
// Error with no context
fmt.Println("Error")
}
Problems:
- No debugging output
- Hardcoded debug statements
- No error context
- No stack traces
Common Issues and Solutions
Goroutine Leaks
package main
import (
"context"
"fmt"
"runtime"
"time"
)
// DetectGoroutineLeaks detects goroutine leaks
func DetectGoroutineLeaks() {
before := runtime.NumGoroutine()
// Run code
time.Sleep(100 * time.Millisecond)
after := runtime.NumGoroutine()
if after > before {
fmt.Printf("Goroutine leak detected: %d -> %d\n", before, after)
}
}
// FixGoroutineLeaks fixes goroutine leaks
func FixGoroutineLeaks(ctx context.Context) {
go func() {
select {
case <-ctx.Done():
return
case <-time.After(10 * time.Second):
// Timeout
}
}()
}
Memory Leaks
package main
import (
"fmt"
"runtime"
)
// DetectMemoryLeaks detects memory leaks
func DetectMemoryLeaks() {
var m1, m2 runtime.MemStats
runtime.ReadMemStats(&m1)
// Run code
for i := 0; i < 1000; i++ {
_ = make([]byte, 1024*1024)
}
runtime.ReadMemStats(&m2)
if m2.Alloc > m1.Alloc {
fmt.Printf("Memory leak detected: %d -> %d\n", m1.Alloc, m2.Alloc)
}
}
Best Practices
1. Use Structured Logging
debugger := NewDebugger(true)
debugger.Logf("x = %d\n", x)
2. Add Context to Errors
return fmt.Errorf("operation failed: %w", err)
3. Use Stack Traces
debugger.PrintStack()
4. Monitor Resources
debugger.PrintMemStats()
debugger.PrintGoroutines()
Common Pitfalls
1. No Debug Output
Always provide debugging information.
2. Hardcoded Debug Statements
Make debugging configurable.
3. No Error Context
Always provide error context.
4. No Resource Monitoring
Monitor memory and goroutines.
Resources
Summary
Effective debugging is essential. Key takeaways:
- Use structured logging
- Add context to errors
- Use debugging tools
- Monitor resources
- Detect leaks early
- Use stack traces
- Test thoroughly
By mastering debugging, you can quickly resolve issues.
Comments