Introduction: Why Go Performance Matters
Go (Golang) is a statically typed compiled language designed for high-load systems. However, even in Go you can write a slow application if you don't consider the specifics of garbage collector (GC) operation, memory management, and concurrency. In this article, we'll break down key optimization techniques: from profiling to fine-tuning the GC. You'll learn how to turn "raw" code into a performant production-ready solution.
1. Profiling: Finding Bottlenecks
The first step of optimization is diagnostics. Go provides the built-in net/http/pprof package for collecting CPU, memory, lock, and goroutine profiles.
Starting Profiling
Add to main.go:
import ( _ "net/http/pprof" "net/http")
func main() { go func() { http.ListenAndServe("localhost:6060", nil) }() // your main code}Then run:go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
Analyzing CPU Profile
After collecting data, an interactive console will open. Use the commands:
top10— shows the 10 "hottest" functions;web— visualizes the call graph in the browser;list <func_name>— shows line-by-line CPU usage.
Example output:
Showing nodes accounting for 80.60s, 85.12% of 94.70s total flat flat% sum% cum cum% 20.30s 21.44% 21.44% 20.30s 21.44% runtime.memmove 15.10s 15.94% 37.38% 15.10s 15.94% runtime.mallocgcIf you see runtime.memmove or mallocgc at the top — the problem is frequent memory allocation.
2. Memory Management and GC
The garbage collector in Go is "stop-the-world", but since Go 1.5 it has become parallel. Nevertheless, frequent allocations increase the load on the GC.
Avoid Unnecessary Allocations
Bad example:
func sum(values []int) int { total := 0 for _, v := range values { total += v } // slice is passed by reference, but if you create a copy — that's an allocation return total}Good example — use buffered channels and object pools:
var bufferPool = sync.Pool{ New: func() interface{} { return make([]byte, 1024) },}
func process() { buf := bufferPool.Get().([]byte) defer bufferPool.Put(buf) // work with buf}GC Tuning
The GOGC environment variable controls collection frequency. Default is 100 (collection starts when heap size doubles). Increase to 200–500 for applications with many allocations:
export GOGC=200./myappBut be careful: too high GOGC can lead to peak memory consumption.
3. Concurrency: Goroutines and Channels
Go is famous for lightweight goroutines, but incorrect usage leads to leaks and deadlocks.
Limit the Number of Goroutines
Use a worker pool:
func workerPool(jobs []int, numWorkers int) { jobsChan := make(chan int, len(jobs)) results := make(chan int, len(jobs))
for w := 0; w < numWorkers; w++ { go func() { for job := range jobsChan { results <- processJob(job) // heavy work } }() }
for _, j := range jobs { jobsChan <- j } close(jobsChan)
for i := 0; i < len(jobs); i++ { <-results }}Avoid Blocking and Deadlocks
Use select with timeouts:
select {case res := <-resultChan: return rescase <-time.After(5 * time.Second): return nil