https://go.dev/doc/diagnostics
Profiling identifies the expensive or frequently called parts of your code — where CPU time goes, how memory is allocated, how many goroutines/threads exist, etc. It answers "what is my program spending resources on?"
Go's runtime exposes profiling data via the runtime/pprof package, which you then inspect using the go tool pprof visualizer. Data can be collected two main ways:
go test -cpuprofile / memprofile.net/http/pprof package.Built-in profile types:
runtime.SetBlockProfileRate.runtime.SetMutexProfileFraction.How to enable profiling in a live HTTP server — just blank-import net/http/pprof and it registers diagnostic endpoints on the default mux under /debug/pprof/:
import (
"log"
"net/http"
_ "net/http/pprof" // registers /debug/pprof/* handlers
)
func main() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}
You can then pull a 30-second CPU profile straight from the running process:
go tool pprof <http://localhost:6060/debug/pprof/profile?seconds=30>
Want the pprof handlers on a custom path/port instead of the default mux? Register them manually:
package main
import (
"log"
"net/http"
"net/http/pprof"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/custom_debug_path/profile", pprof.Profile)
log.Fatal(http.ListenAndServe(":7777", mux))
}
Is it safe to profile production? Yes — but some profiles (especially CPU) add overhead, so expect a small performance hit. A common pattern: pick a random replica out of many, profile it for X seconds every Y seconds, save the results, and repeat — rather than profiling every instance continuously.
Visualizing profiles — go tool pprof supports several output modes once you have a profile file:
go tool pprof -top myprofile.prof # text listing of most expensive calls
go tool pprof -web myprofile.prof # graph visualization
go tool pprof -weblist=funcName myprofile.prof # line-by-line source cost
You're not limited to the built-in profiles — you can define custom ones with pprof.Profile and inspect them with the same tooling.
golang.org/x/net/trace package as a lightweight tracing backend with a simple dashboard.go tool trace.
Capture a trace like this:
import (
"os"
"runtime/trace"
)
func main() {
f, _ := os.Create("trace.out")
defer f.Close()
trace.Start(f)
defer trace.Stop()
// ... your program logic ...
}
go tool trace trace.out
Distributed tracing helps you:
Go doesn't automatically intercept every function call to create spans — you have to manually instrument code to start, annotate, and end trace spans.
Trace context (IDs, tags) is typically propagated through context.Context, since there's no single standard trace-header format — each tracing backend (Jaeger, Zipkin, OpenTelemetry, etc.) provides its own propagation helpers.
func handler(ctx context.Context) {
ctx, span := tracer.Start(ctx, "handler")
defer span.End()
doWork(ctx) // pass ctx down so the trace continues
}
net/http/httptrace.ClientTrace is a standard-library hook that lets you observe low-level events (DNS lookup, connection reuse, etc.) in the lifecycle of an outgoing HTTP request — useful for feeding into your own traces.