Performance tuning in Go begint niet bij code aanpassen, maar bij meten. Zonder data optimaliseer je op gevoel, en dat levert zelden winst op. Go biedt krachtige ingebouwde tools zoals pprof, go test -bench en de runtime tracer waarmee je precies ziet waar de tijd en het geheugen naartoe gaan.
In deze handleiding leer je in 7 concrete stappen hoe je een Go applicatie analyseert, knelpunten vindt en gericht optimaliseert. We kijken naar benchmarks, CPU- en memory-profiling, allocatiereductie en concurrency-tuning.
Stap 1: meet eerst, optimaliseer daarna
De gouden regel van performance tuning: raak geen code aan zonder cijfers. Een "snellere" implementatie die 5% verbetering oplevert op een stuk code dat maar 0,1% van de runtime gebruikt, is verspilde moeite.
Begin met een baseline. Draai je applicatie onder realistische load en meet:
- Requests per seconde of doorvoer
- Latency percentielen (p50, p95, p99)
- CPU-gebruik en geheugenverbruik
- Garbage collection pauzes
Sla deze cijfers op. Elke optimalisatie vergelijk je met deze baseline. Zonder vergelijking weet je nooit of je verbetert of juist verslechtert.
Gebruik voor productie-metrics de technieken uit Logging en observability in Go om latency en resourcegebruik continu te monitoren.
Stap 2: schrijf benchmarks voor hot paths
Go heeft benchmarks ingebouwd in het testing pakket. Een benchmark is een functie die begint met Benchmark en een *testing.B parameter ontvangt.
func BenchmarkParseJSON(b *testing.B) {
data := []byte(`{"name":"Sander","age":30}`)
b.ResetTimer()
for i := 0; i < b.N; i++ {
var u User
json.Unmarshal(data, &u)
}
}
Draai met:
go test -bench=. -benchmem -count=5
De -benchmem vlag toont allocaties per operatie, -count=5 draait elke benchmark meerdere keren zodat je variatie ziet. Gebruik benchstat om voor- en na-resultaten statistisch te vergelijken.
Meer over teststructuur en tabletests vind je in Testing in Go.
Stap 3: CPU-profiling met pprof
Als je weet welke functie traag aanvoelt, bewijs het met een profiel. Voeg pprof toe aan je applicatie:
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// je gewone applicatie
}
Verzamel vervolgens 30 seconden CPU-data tijdens load:
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
In de interactieve shell gebruik je top om de zwaarste functies te zien en web voor een visuele call graph. Kijk naar flat (tijd in de functie zelf) versus cum (inclusief aanroepen). Een hoge flat wijst op een directe kandidaat voor optimalisatie.
Voor benchmarks werkt profiling direct:
go test -bench=BenchmarkParseJSON -cpuprofile=cpu.out
go tool pprof cpu.out
Stap 4: memory-profiling en allocaties verminderen
Veel Go performance-problemen komen niet door trage code, maar door druk op de garbage collector. Elke allocatie kost tijd en triggert uiteindelijk GC-werk.
Draai een memory-profiel:
go tool pprof http://localhost:6060/debug/pprof/heap
Of meet allocaties per benchmark met -benchmem. Veel voorkomende oorzaken van onnodige allocaties:
- String-concatenatie in loops, gebruik
strings.Builderin plaats van+= - Slices zonder capaciteit,
make([]T, 0, n)voorkomt hergroeien - Interface-conversies van kleine types, elke conversie kopieert naar de heap
- Onnodige pointer-returns bij kleine structs, een value return kan op de stack blijven
Een veelvoorkomende winst: vervang fmt.Sprintf("%d", n) door strconv.Itoa(n). Dat scheelt vaak 2–3 allocaties per aanroep.
Stap 5: gebruik sync.Pool voor hergebruik
Voor buffers en andere kortlevende objecten die je vaak opnieuw nodig hebt, is sync.Pool een krachtig middel om GC-druk te verlagen.
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func handle(w http.ResponseWriter, r *http.Request) {
buf := bufPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufPool.Put(buf)
}()
// gebruik buf
}
Pas wel op: sync.Pool werkt alleen als objecten vaak hergebruikt worden. Voor zeldzame of zware objecten voegt het juist overhead toe. Meet altijd het effect met een benchmark.
Stap 6: concurrency op de juiste plek
Goroutines zijn goedkoop, maar niet gratis. Een veelgemaakte fout: voor elke kleine taak een goroutine starten, ook als die taak CPU-gebonden en al snel klaar is. Het resultaat is meer scheduler-overhead en weinig winst.
Concurrency loont vooral bij:
- I/O-bound werk (database, HTTP, file)
- Onafhankelijk CPU-werk dat verdeeld kan worden over cores
- Pipelines waarin producers en consumers parallel lopen
Gebruik een worker pool met een begrensd aantal workers in plaats van unbounded goroutines:
jobs := make(chan Job, 100)
for w := 0; w < runtime.NumCPU(); w++ {
go worker(jobs)
}
Voor meer context over goroutines en channels, zie Goroutines en concurrency basics en Channels diep uitgelegd.
Stap 7: database- en netwerk-optimalisatie
In webservices zit de traagste code vaak niet in Go, maar in de laag eromheen. Een trage query of onnodig HTTP-verkeer domineert elke micro-optimalisatie.
Checklist voor databasewerk:
- Connection pooling, stel
SetMaxOpenConnsenSetMaxIdleConnscorrect in - Prepared statements, hergebruik voor queries die vaak herhalen
- Indexes, controleer in je database of queries indexen gebruiken
- Batch-operaties, vervang N losse inserts door één bulk-insert
- Selecteer alleen nodige kolommen, vermijd
SELECT *
Meer hierover lees je in Database toegang in Go.
Voor HTTP-clients: hergebruik één http.Client in plaats van er per request een nieuwe te maken, en configureer Transport.MaxIdleConnsPerHost voor services die je vaak aanroept. Graceful shutdown en efficiënte routing behandelen we in HTTP servers bouwen in Go.
Veelgemaakte valkuilen
Ook ervaren Go-developers trappen in deze:
- Micro-optimalisatie zonder profiel, urenlang sleutelen aan een functie die slechts 1% van de runtime gebruikt
- Goroutines per verzoek zonder begrenzing, leidt tot geheugenexplosies onder load
- Defer in hot loops,
deferheeft lichte overhead, vermijd in miljoenen iteraties - Reflectie in kritieke paden,
reflectis flexibel maar traag; genereer code of gebruik type-assertions - Negeren van compiler-optimalisaties, de Go-compiler inlinet en vereenvoudigt veel; geforceerde "slimme" code kan die optimalisaties juist blokkeren
Een laatste tip: kijk naar de officiële Go performance documentatie en de pprof handleiding voor diepgaande tooling-uitleg.
Veelgestelde vragen
Wanneer moet ik beginnen met performance tuning in Go?
Pas als je een gemeten probleem hebt. Begin altijd met benchmarks en profiling data voordat je optimaliseert. Vroegtijdige optimalisatie maakt code onleesbaar zonder dat je zeker weet of het helpt.
Wat is het verschil tussen pprof CPU en memory profiling?
CPU profiling toont waar je programma tijd doorbrengt. Memory profiling toont waar allocaties plaatsvinden. Voor latency-problemen begin je met CPU, voor GC-druk of hoog geheugengebruik met memory profiling.
Helpt een object pool (sync.Pool) altijd?
Nee. sync.Pool is handig bij veel kortlevende allocaties van hetzelfde type, bijvoorbeeld buffers. Voor zeldzame of langlevende objecten voegt het overhead toe en kan het zelfs trager zijn.
Moet ik goroutines gebruiken om performance te verbeteren?
Alleen bij werk dat echt parallel of I/O-gebonden is. Te veel goroutines kosten geheugen en scheduler-overhead. Meet altijd of concurrency daadwerkelijk winst oplevert voor jouw workload.
Wat is een realistische performance-winst na tuning?
Dat verschilt sterk per applicatie. Hot paths optimaliseren levert vaak 2x tot 10x op, soms meer. Code die al efficiënt is, levert slechts enkele procenten. Meet voor en na om het verschil aan te tonen.
Conclusie
Performance tuning in Go draait om discipline: meten, profileren, gericht aanpassen en opnieuw meten. De tools zijn er, pprof, benchmarks, de trace viewer, maar ze helpen alleen als je ze consistent gebruikt.
Begin klein. Kies één hot path, schrijf een benchmark, profileer en optimaliseer tot je een meetbare verbetering hebt. Herhaal dat proces en je Go-applicatie wordt stap voor stap sneller, zonder dat je code onleesbaar wordt door speculatieve optimalisaties.