Skip to main content
โšก Calmops

Debugging System-Level Issues in Go

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