This episode measures and optimizes Go performance: profiling with pprof, benchmarking with go test -bench, allocation analysis with escape analysis, plus tuning techniques such as buffer reuse and zero allocation patterns.

Episode 13 answers one question: is your Go application fast? Not by guessing, but by measuring. Go provides world-class profiling and benchmarking tooling right from its official toolchain.
You will learn four things: CPU and memory profiling with pprof, benchmarking with go test -bench, understanding memory allocation and escape analysis, and performance tuning techniques such as buffer reuse and zero allocation patterns. The focus is always: measure first, then optimize what matters.
net/http/pprof provides profiling endpoints for running applications. A blank import is enough to enable them on an HTTP server.
package main
import (
"net/http"
_ "net/http/pprof"
)
func main() {
http.ListenAndServe(":8080", nil)
}While the application runs, access /debug/pprof/ for the list of profiles, /debug/pprof/profile for a 30-second CPU profile, and /debug/pprof/heap for a heap snapshot. Don't enable pprof on public endpoints without authentication.
Collect a profile and then analyze it with go tool pprof:
curl -o cpu.prof http://localhost:8080/debug/pprof/profile?seconds=30
go tool pprof cpu.profInside the interactive shell, the top command shows the functions that use the most CPU, list namaFungsi shows it line by line, and web visualizes the call graph. After looking at the profile, optimize only the functions that are truly dominant.
Benchmarks (from episode 6) are the basis for comparison. When optimizing, measure before and after, then compare:
func BenchmarkGabungBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
for j := 0; j < 100; j++ {
sb.WriteString("data")
}
_ = sb.String()
}
}Run it with go test -bench=. -benchmem -count=5 ./... for more stable results. -count repeats the benchmark so network and OS noise can be averaged out.
The Go compiler can eliminate code whose results are never used. Every benchmark result should be stored in a package-level variable so it isn't optimized away.
var hasilBenchmark string
func BenchmarkKode(b *testing.B) {
var s string
for i := 0; i < b.N; i++ {
s = gabungkan()
}
hasilBenchmark = s
}Escape analysis determines whether a variable lives on the stack (cheap) or the heap (expensive, needs GC). Variables that "escape" out of a function — returned, stored in a pointer, or captured by a closure — are allocated on the heap.
func buatPointer() *int {
n := 42
return &n
}Run go build -gcflags="-m" main.go to see the compiler's decisions. If the output mentions moved to heap, the object is allocated on the heap. Reducing heap allocations directly lowers the garbage collector's workload. Alternatively, go build -gcflags="-m" main.go still produces a binary even while showing the allocation analysis.
Every heap allocation adds GC work: the more allocations, the more often the GC runs. That's why zero allocation patterns on hot paths can significantly improve throughput.
For repeated data reads, allocate a buffer once and reuse it many times. sync.Pool even lets buffers be shared across goroutines:
var bufPool = sync.Pool{
New: func() any {
return make([]byte, 1024)
},
}
func proses(data []byte) {
buf := bufPool.Get().([]byte)
defer bufPool.Put(buf)
copy(buf, data)
// proses data
}sync.Pool returns objects that were created before and discards them when the GC runs. It suits temporary buffers, not state that must survive.
Repeated append triggers reallocation when the capacity is full. Set the initial capacity when creating a slice so the allocation happens only once:
hasil := make([]int, 0, 1000)
for _, v := range sumber {
hasil = append(hasil, v)
}With make([]int, 0, 1000), the capacity is reserved upfront. For thousands of elements, this avoids dozens of unnecessary reallocations.
A few more practices: reuse structs, avoid pointers to small data, and use range over values for immutable data. Never optimize before a profile shows a problem — premature optimization only adds complexity.
Episode 13 equipped you with the measurement and optimization cycle: profiling with pprof and go tool pprof, benchmarking with go test -bench and -benchmem, understanding escape analysis and heap allocation, plus the techniques of buffer reuse, sync.Pool, and proper slice capacity.
Key takeaways:
-benchmem and -count for stable results.sync.Pool reuses buffers across goroutines.append doesn't reallocate often.In the next episode we will discuss observability, logging, and tracing — structured logging with zap, logrus, or zerolog, distributed tracing with OpenTelemetry, and structured metrics with the Prometheus client. Your applications will be introspectable while running in production.