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
- Learn about Error Handling
- Explore Configuration Management
- Study Security