Go Application Performance Optimization: Tips and Examples

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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.mallocgc

If 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./myapp

But 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                

Blogs

Book Recommendations