1. go
  2. /best practices
  3. /performance

Performance Best Practices in Go

Writing high-performance Go code requires understanding the language's performance characteristics and using appropriate optimization techniques. This guide covers best practices for optimizing Go applications.

Profiling

1. CPU Profiling

Profile CPU usage:

package main

import (
    "os"
    "runtime/pprof"
)

func main() {
    // Create CPU profile
    f, err := os.Create("cpu.prof")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    
    if err := pprof.StartCPUProfile(f); err != nil {
        log.Fatal(err)
    }
    defer pprof.StopCPUProfile()
    
    // Run your program
    heavyComputation()
}

// Using net/http/pprof
import _ "net/http/pprof"

func main() {
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    
    // Run your program
}

2. Memory Profiling

Profile memory allocations:

func main() {
    // Create memory profile
    f, err := os.Create("mem.prof")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    
    // Run program
    heavyMemoryOperation()
    
    // Write memory profile
    if err := pprof.WriteHeapProfile(f); err != nil {
        log.Fatal(err)
    }
}

Benchmarking

1. Writing Benchmarks

Create effective benchmarks:

func BenchmarkOperation(b *testing.B) {
    // Setup
    data := makeTestData()
    
    // Reset timer after setup
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        result := Operation(data)
        // Prevent compiler optimization
        runtime.KeepAlive(result)
    }
}

// Benchmark with different sizes
func BenchmarkOperationSize(b *testing.B) {
    sizes := []int{100, 1000, 10000}
    
    for _, size := range sizes {
        b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
            data := makeTestData(size)
            b.ResetTimer()
            
            for i := 0; i < b.N; i++ {
                Operation(data)
            }
        })
    }
}

2. Memory Benchmarks

Measure memory allocations:

func BenchmarkAllocs(b *testing.B) {
    b.ReportAllocs()
    
    for i := 0; i < b.N; i++ {
        data := make([]int, 1000)
        processData(data)
    }
}

Memory Management

1. Reducing Allocations

Minimize garbage collection pressure:

// Bad: New allocation per call
func processString(s string) string {
    var result string
    for _, c := range s {
        result += string(c) // Creates new string each iteration
    }
    return result
}

// Good: Use strings.Builder
func processString(s string) string {
    var builder strings.Builder
    builder.Grow(len(s)) // Pre-allocate capacity
    
    for _, c := range s {
        builder.WriteRune(c)
    }
    return builder.String()
}

// Bad: Unnecessary allocations
func processItems(items []Item) []Result {
    results := make([]Result, 0) // Unknown capacity
    for _, item := range items {
        results = append(results, process(item))
    }
    return results
}

// Good: Pre-allocate slice
func processItems(items []Item) []Result {
    results := make([]Result, 0, len(items))
    for _, item := range items {
        results = append(results, process(item))
    }
    return results
}

2. Object Pooling

Use sync.Pool for frequently allocated objects:

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func processData(data []byte) error {
    // Get buffer from pool
    buf := bufferPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufferPool.Put(buf)
    }()
    
    // Use buffer
    if _, err := buf.Write(data); err != nil {
        return err
    }
    
    return process(buf)
}

Concurrency Optimization

1. Goroutine Management

Efficient goroutine usage:

// Worker pool pattern
func ProcessItems(items []Item) []Result {
    numWorkers := runtime.GOMAXPROCS(0)
    jobs := make(chan Item, len(items))
    results := make(chan Result, len(items))
    
    // Start workers
    var wg sync.WaitGroup
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for item := range jobs {
                results <- process(item)
            }
        }()
    }
    
    // Send jobs
    for _, item := range items {
        jobs <- item
    }
    close(jobs)
    
    // Wait for workers
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // Collect results
    var processed []Result
    for result := range results {
        processed = append(processed, result)
    }
    
    return processed
}

2. Channel Optimization

Efficient channel usage:

// Bad: Unnecessary channel allocation
func generator() <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch)
        for i := 0; i < 100; i++ {
            ch <- i
        }
    }()
    return ch
}

// Good: Buffered channel
func generator() <-chan int {
    ch := make(chan int, 100)
    go func() {
        defer close(ch)
        for i := 0; i < 100; i++ {
            ch <- i
        }
    }()
    return ch
}

Data Structure Optimization

1. Slice Operations

Efficient slice handling:

// Bad: Copying slices
func removeItem(s []int, i int) []int {
    return append(s[:i], s[i+1:]...)
}

// Good: Avoid memory leak
func removeItem(s []int, i int) []int {
    s[i] = s[len(s)-1]
    return s[:len(s)-1]
}

// Bad: Growing slice
func appendItems(items []Item) []Item {
    for i := 0; i < 1000; i++ {
        items = append(items, Item{})
    }
    return items
}

// Good: Pre-allocate
func appendItems(items []Item) []Item {
    if cap(items)-len(items) < 1000 {
        newItems := make([]Item, len(items), len(items)+1000)
        copy(newItems, items)
        items = newItems
    }
    for i := 0; i < 1000; i++ {
        items = append(items, Item{})
    }
    return items
}

2. Map Operations

Efficient map usage:

// Bad: Map with pointer values
type Cache map[string]*Item

// Good: Map with values
type Cache map[string]Item

// Bad: Growing map
func buildMap(items []Item) map[string]Item {
    m := make(map[string]Item) // Unknown size
    for _, item := range items {
        m[item.ID] = item
    }
    return m
}

// Good: Pre-allocate map
func buildMap(items []Item) map[string]Item {
    m := make(map[string]Item, len(items))
    for _, item := range items {
        m[item.ID] = item
    }
    return m
}

I/O Optimization

1. Buffered I/O

Use buffered operations:

// Bad: Unbuffered reading
func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    
    return ioutil.ReadAll(f)
}

// Good: Buffered reading
func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    
    reader := bufio.NewReader(f)
    return reader.ReadBytes('\n')
}

2. Network I/O

Optimize network operations:

// Bad: Creating new client per request
func makeRequest(url string) error {
    client := &http.Client{}
    resp, err := client.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    return nil
}

// Good: Reuse client
var client = &http.Client{
    Timeout: 10 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 100,
        IdleConnTimeout:     90 * time.Second,
    },
}

func makeRequest(url string) error {
    resp, err := client.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    return nil
}

Next Steps