Production-grade Go applications require robust debugging capabilities. I’ve developed and maintained numerous Go services, and these techniques have proven invaluable in identifying and resolving issues quickly.
Log Management is fundamental for production debugging. I recommend using structured logging with context:
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("processing_request",
zap.String("request_id", req.ID),
zap.Int("user_id", user.ID),
zap.Duration("latency", time.Since(start)))
Runtime profiling provides insights into application behavior. I always enable pprof in production services:
import (
"net/http"
_ "net/http/pprof"
)
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
For CPU profiling, I use this pattern:
f, err := os.Create("cpu.prof")
if err != nil {
log.Fatal(err)
}
defer f.Close()
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
Memory analysis is crucial. I implement periodic memory statistics logging:
func logMemStats() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
log.Printf("Alloc = %v MiB", m.Alloc / 1024 / 1024)
log.Printf("TotalAlloc = %v MiB", m.TotalAlloc / 1024 / 1024)
log.Printf("Sys = %v MiB", m.Sys / 1024 / 1024)
log.Printf("NumGC = %v", m.NumGC)
}
Error tracking with context helps identify issue sources:
type ErrorWithContext struct {
Err error
Context map[string]interface{}
}
func (e *ErrorWithContext) Error() string {
return fmt.Sprintf("%v (context: %v)", e.Err, e.Context)
}
func WrapError(err error, context map[string]interface{}) error {
return &ErrorWithContext{
Err: err,
Context: context,
}
}
Distributed tracing improves visibility across services:
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
span := trace.SpanFromContext(r.Context())
defer span.End()
span.SetAttributes(
attribute.String("http.method", r.Method),
attribute.String("http.url", r.URL.String()),
)
next.ServeHTTP(w, r)
})
}
Performance metrics collection provides operational insights:
type Metrics struct {
requestCounter *prometheus.CounterVec
requestDuration *prometheus.HistogramVec
activeGoroutines prometheus.Gauge
}
func NewMetrics() *Metrics {
return &Metrics{
requestCounter: prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests processed",
},
[]string{"method", "endpoint", "status"},
),
requestDuration: prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
},
[]string{"method", "endpoint"},
),
activeGoroutines: prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "goroutines_active",
Help: "Number of active goroutines",
},
),
}
}
Remote debugging capabilities are essential:
func enableRemoteDebugging() {
listener, err := net.Listen("tcp", "localhost:4000")
if err != nil {
log.Fatal(err)
}
debugger := debugger.New(&debugger.Config{
Listener: listener,
ProcessArgs: []string{"./myapp"},
})
if err := debugger.Run(); err != nil {
log.Fatal(err)
}
}
Resource monitoring helps prevent outages:
type ResourceMonitor struct {
threshold float64
interval time.Duration
}
func (rm *ResourceMonitor) Start() {
ticker := time.NewTicker(rm.interval)
go func() {
for range ticker.C {
var m runtime.MemStats
runtime.ReadMemStats(&m)
if float64(m.Alloc)/float64(m.Sys) > rm.threshold {
log.Printf("Memory usage above threshold: %v%%",
float64(m.Alloc)/float64(m.Sys)*100)
}
}
}()
}
Panic recovery ensures application stability:
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
stack := make([]byte, 4096)
stack = stack[:runtime.Stack(stack, false)]
log.Printf("panic: %v\n%s", err, stack)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
These techniques form a comprehensive debugging strategy. Implementation varies based on specific requirements, but these patterns provide a solid foundation for maintaining production Go applications.
Remember to regularly review and update debugging tools and strategies as your application evolves. Effective debugging in production requires both proactive monitoring and reactive investigation capabilities.