Introduction: Why Go Application Performance Matters
Go (Golang) is renowned for its speed and efficiency, but even in this language, you can write slow code. Performance optimization is not about premature micro-optimization, but about the conscious application of best practices that make your application fast, scalable, and resource-efficient. For beginner Go developers, understanding the basics of profiling and optimization is a key step towards creating production-ready services.
In this article, we will cover four practical optimization approaches you can apply today: from configuring the garbage collector to working correctly with goroutines.
1. Profiling: Find Bottlenecks Before Optimizing
Before changing anything, you need to understand where performance is being lost. Go provides the built-in pprof package for profiling CPU, memory, blocking, and goroutines.
Running CPU Profiling
Add profiler initialization to your code:
package main
import ( "os" "runtime/pprof")
func main() { f, _ := os.Create("cpu.prof") pprof.StartCPUProfile(f) defer pprof.StopCPUProfile()
// Your code heavyFunction()}After running the program, execute in the terminal:
go tool pprof -http=:8080 cpu.profA visualization will open in your browser, showing which functions consume the most CPU time. This is your first step towards informed optimization.
Memory Profiling
To find leaks and excessive memory allocation, use:
import "runtime/pprof"
func main() { f, _ := os.Create("mem.prof") defer f.Close()
// After code execution runtime.GC() pprof.WriteHeapProfile(f)}Analyze the result using go tool pprof and pay attention to functions with a high number of allocations.
2. Memory Management: Garbage Collector and Allocations
Go uses an efficient garbage collector (GC), but its operation can slow down the application with frequent allocations. Here's how to minimize the load.
Reducing the Number of Allocations
Avoid creating temporary objects in hot loops. Compare two approaches:
// Bad: new allocation on each iterationfunc sumBad(nums []int) int { total := 0 for _, n := range nums { total += n _ = make([]byte, 1024) // unnecessary allocation } return total}
// Good: no unnecessary allocationsfunc sumGood(nums []int) int { total := 0 for _, n := range nums { total += n } return total}Configuring GC
By default, the GC runs when the heap size doubles compared to the previous cycle. You can control this via the GOGC environment variable:
GOGC=100 ./myapp // Standard behaviorGOGC=200 ./myapp // Fewer GC cycles, but more memoryGOGC=off ./myapp // Disable GC (only for short-lived tasks)For real-time systems sensitive to latency, increase GOGC to 200-400.
3. Concurrency: Goroutines and Channels Without Pain
Go prides itself on its concurrency model, but incorrect use of goroutines can lead to performance degradation.
Goroutine Pool Instead of Unlimited Creation
Creating 100,000 goroutines is a bad idea. Use buffered channels and a worker pool:
func workerPool(jobs []int, numWorkers int) { jobsChan := make(chan int, len(jobs)) results := make(chan int, len(jobs))
// Start a fixed number of workers for w := 0; w < numWorkers; w++ { go worker(jobsChan, results) }
// Send tasks for _, j := range jobs { jobsChan