Performance Patterns
Collection of proven performance patterns, anti-patterns, and best practices for Go applications with practical examples and benchmarks.
Core Performance Patterns
Pre-allocation Pattern
Pattern: Allocate slices and maps with known capacity to avoid reallocations.
// ❌ Anti-pattern: Growing slice
func processItemsBad(items []Item) []Result {
var results []Result // Zero capacity
for _, item := range items {
result := processItem(item)
results = append(results, result) // May trigger multiple reallocations
}
return results
}
// ✅ Pattern: Pre-allocated slice
func processItemsGood(items []Item) []Result {
results := make([]Result, 0, len(items)) // Pre-allocate capacity
for _, item := range items {
result := processItem(item)
results = append(results, result) // No reallocations
}
return results
}
// Benchmark comparison
func BenchmarkPreallocation(b *testing.B) {
items := make([]Item, 1000)
for i := range items {
items[i] = Item{ID: i}
}
b.Run("WithoutPreallocation", func(b *testing.B) {
for i := 0; i < b.N; i++ {
results := processItemsBad(items)
_ = results
}
})
b.Run("WithPreallocation", func(b *testing.B) {
for i := 0; i < b.N; i++ {
results := processItemsGood(items)
_ = results
}
})
}
Results: Pre-allocation typically reduces allocations by 10-100x and improves performance by 2-5x.
String Builder Pattern
Pattern: Use strings.Builder for efficient string concatenation.
// ❌ Anti-pattern: String concatenation
func buildMessageBad(parts []string) string {
var message string
for _, part := range parts {
message += part + " " // Creates new string each time
}
return strings.TrimSpace(message)
}
// ✅ Pattern: strings.Builder
func buildMessageGood(parts []string) string {
var builder strings.Builder
// Pre-allocate if size is known
totalLen := 0
for _, part := range parts {
totalLen += len(part) + 1 // +1 for space
}
builder.Grow(totalLen)
for i, part := range parts {
if i > 0 {
builder.WriteByte(' ')
}
builder.WriteString(part)
}
return builder.String()
}
// ✅ Alternative: Join for simple cases
func buildMessageSimple(parts []string) string {
return strings.Join(parts, " ")
}
Object Pool Pattern
Pattern: Reuse expensive objects to reduce allocations and GC pressure.
// Object pool for buffers
var bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 0, 4096) // 4KB initial capacity
},
}
// ✅ Pattern: Object pooling
func processDataWithPool(data []byte) []byte {
buffer := bufferPool.Get().([]byte)
defer func() {
buffer = buffer[:0] // Reset length, keep capacity
bufferPool.Put(buffer)
}()
// Use buffer for processing
buffer = append(buffer, data...)
// Apply transformations
for i := range buffer {
buffer[i] = transform(buffer[i])
}
// Return copy since buffer will be reused
result := make([]byte, len(buffer))
copy(result, buffer)
return result
}
// Custom pool with type safety
type BufferPool struct {
pool sync.Pool
}
func NewBufferPool(initialSize int) *BufferPool {
return &BufferPool{
pool: sync.Pool{
New: func() interface{} {
return make([]byte, 0, initialSize)
},
},
}
}
func (bp *BufferPool) Get() []byte {
return bp.pool.Get().([]byte)
}
func (bp *BufferPool) Put(buf []byte) {
if cap(buf) > 64*1024 { // Don't keep very large buffers
return
}
bp.pool.Put(buf[:0])
}
Worker Pool Pattern
Pattern: Control concurrency with a fixed number of workers to prevent resource exhaustion.
// ✅ Pattern: Worker pool
type WorkerPool struct {
workers int
taskQueue chan Task
resultChan chan Result
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
type Task func() Result
type Result struct {
Value interface{}
Error error
}
func NewWorkerPool(workers, queueSize int) *WorkerPool {
ctx, cancel := context.WithCancel(context.Background())
return &WorkerPool{
workers: workers,
taskQueue: make(chan Task, queueSize),
resultChan: make(chan Result, queueSize),
ctx: ctx,
cancel: cancel,
}
}
func (wp *WorkerPool) Start() {
for i := 0; i < wp.workers; i++ {
wp.wg.Add(1)
go wp.worker()
}
}
func (wp *WorkerPool) worker() {
defer wp.wg.Done()
for {
select {
case task := <-wp.taskQueue:
result := task()
select {
case wp.resultChan <- result:
case <-wp.ctx.Done():
return
}
case <-wp.ctx.Done():
return
}
}
}
func (wp *WorkerPool) Submit(task Task) error {
select {
case wp.taskQueue <- task:
return nil
case <-wp.ctx.Done():
return wp.ctx.Err()
default:
return errors.New("task queue full")
}
}
func (wp *WorkerPool) Results() <-chan Result {
return wp.resultChan
}
func (wp *WorkerPool) Stop() {
wp.cancel()
close(wp.taskQueue)
wp.wg.Wait()
close(wp.resultChan)
}
// Usage example
func processWithWorkerPool(tasks []Task) []Result {
pool := NewWorkerPool(runtime.NumCPU(), len(tasks))
pool.Start()
defer pool.Stop()
// Submit all tasks
for _, task := range tasks {
pool.Submit(task)
}
// Collect results
results := make([]Result, 0, len(tasks))
for i := 0; i < len(tasks); i++ {
result := <-pool.Results()
results = append(results, result)
}
return results
}
Memory Optimization Patterns
Zero Allocation JSON Parsing
Pattern: Parse JSON without allocations using streaming or pre-allocated structures.
// ✅ Pattern: Pre-allocated struct with json.Decoder
type Event struct {
ID int64 `json:"id"`
Type string `json:"type"`
Timestamp time.Time `json:"timestamp"`
Data []byte `json:"data"`
}
func parseEventsEfficient(r io.Reader) ([]Event, error) {
decoder := json.NewDecoder(r)
var events []Event
// Expect array start
token, err := decoder.Token()
if err != nil {
return nil, err
}
if delim, ok := token.(json.Delim); !ok || delim != '[' {
return nil, errors.New("expected array")
}
// Parse each object
for decoder.More() {
var event Event
if err := decoder.Decode(&event); err != nil {
return nil, err
}
events = append(events, event)
}
// Expect array end
if _, err := decoder.Token(); err != nil {
return nil, err
}
return events, nil
}
// ✅ Pattern: Custom JSON parser for hot paths
func parseIDFromJSON(data []byte) (int64, error) {
// Simple parser for {"id": 12345, ...}
start := bytes.Index(data, []byte(`"id":`))
if start == -1 {
return 0, errors.New("id not found")
}
start += 5 // len(`"id":`)
// Skip whitespace
for start < len(data) && (data[start] == ' ' || data[start] == '\t') {
start++
}
end := start
for end < len(data) && data[end] >= '0' && data[end] <= '9' {
end++
}
if end == start {
return 0, errors.New("invalid id format")
}
return strconv.ParseInt(string(data[start:end]), 10, 64)
}
Memory-Mapped File Pattern
Pattern: Use memory mapping for large file processing.
// ✅ Pattern: Memory-mapped file processing
func processLargeFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return err
}
size := stat.Size()
// Memory map the file (platform-specific implementation)
data, err := syscall.Mmap(int(file.Fd()), 0, int(size),
syscall.PROT_READ, syscall.MAP_PRIVATE)
if err != nil {
return err
}
defer syscall.Munmap(data)
// Process data without loading entire file into memory
return processDataInChunks(data)
}
func processDataInChunks(data []byte) error {
chunkSize := 64 * 1024 // 64KB chunks
for i := 0; i < len(data); i += chunkSize {
end := i + chunkSize
if end > len(data) {
end = len(data)
}
chunk := data[i:end]
if err := processChunk(chunk); err != nil {
return err
}
}
return nil
}
Concurrency Patterns
Pipeline Pattern
Pattern: Process data through stages with goroutines and channels.
// ✅ Pattern: Pipeline processing
func processPipeline(input <-chan RawData) <-chan ProcessedData {
// Stage 1: Validate
validated := make(chan ValidatedData, 100)
go func() {
defer close(validated)
for data := range input {
if valid := validate(data); valid != nil {
validated <- *valid
}
}
}()
// Stage 2: Transform
transformed := make(chan TransformedData, 100)
go func() {
defer close(transformed)
for data := range validated {
transformed <- transform(data)
}
}()
// Stage 3: Enrich
processed := make(chan ProcessedData, 100)
go func() {
defer close(processed)
for data := range transformed {
processed <- enrich(data)
}
}()
return processed
}
// ✅ Pattern: Fan-out/Fan-in
func fanOutFanIn(input <-chan Task, numWorkers int) <-chan Result {
// Fan-out: distribute work to multiple workers
workers := make([]<-chan Result, numWorkers)
for i := 0; i < numWorkers; i++ {
worker := make(chan Result)
workers[i] = worker
go func(output chan<- Result) {
defer close(output)
for task := range input {
output <- processTask(task)
}
}(worker)
}
// Fan-in: merge results from all workers
return mergeChannels(workers...)
}
func mergeChannels(channels ...<-chan Result) <-chan Result {
merged := make(chan Result)
var wg sync.WaitGroup
wg.Add(len(channels))
for _, ch := range channels {
go func(c <-chan Result) {
defer wg.Done()
for result := range c {
merged <- result
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
Rate Limiting Pattern
Pattern: Control resource access rate to prevent system overload.
// ✅ Pattern: Token bucket rate limiter
type RateLimiter struct {
tokens chan struct{}
ticker *time.Ticker
maxTokens int
}
func NewRateLimiter(rate int, burst int) *RateLimiter {
rl := &RateLimiter{
tokens: make(chan struct{}, burst),
ticker: time.NewTicker(time.Second / time.Duration(rate)),
maxTokens: burst,
}
// Fill initial tokens
for i := 0; i < burst; i++ {
rl.tokens <- struct{}{}
}
// Refill tokens
go func() {
defer rl.ticker.Stop()
for range rl.ticker.C {
select {
case rl.tokens <- struct{}{}:
default: // Bucket full
}
}
}()
return rl
}
func (rl *RateLimiter) Allow() bool {
select {
case <-rl.tokens:
return true
default:
return false
}
}
func (rl *RateLimiter) Wait(ctx context.Context) error {
select {
case <-rl.tokens:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// Usage in HTTP handler
func rateLimitedHandler(rl *RateLimiter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !rl.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
// Process request
handleRequest(w, r)
}
}
I/O Optimization Patterns
Buffered Writer Pattern
Pattern: Batch writes to reduce system call overhead.
// ✅ Pattern: Buffered batch writing
type BatchWriter struct {
writer io.Writer
buffer []byte
size int
mu sync.Mutex
}
func NewBatchWriter(w io.Writer, batchSize int) *BatchWriter {
bw := &BatchWriter{
writer: w,
buffer: make([]byte, 0, batchSize),
size: batchSize,
}
// Periodic flush
go func() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for range ticker.C {
bw.Flush()
}
}()
return bw
}
func (bw *BatchWriter) Write(data []byte) error {
bw.mu.Lock()
defer bw.mu.Unlock()
// If adding this data would exceed buffer size, flush first
if len(bw.buffer)+len(data) > bw.size {
if err := bw.flushLocked(); err != nil {
return err
}
}
bw.buffer = append(bw.buffer, data...)
return nil
}
func (bw *BatchWriter) Flush() error {
bw.mu.Lock()
defer bw.mu.Unlock()
return bw.flushLocked()
}
func (bw *BatchWriter) flushLocked() error {
if len(bw.buffer) == 0 {
return nil
}
_, err := bw.writer.Write(bw.buffer)
bw.buffer = bw.buffer[:0] // Reset length, keep capacity
return err
}
Connection Pool Pattern
Pattern: Reuse network connections to reduce establishment overhead.
// ✅ Pattern: Connection pool
type ConnectionPool struct {
factory func() (net.Conn, error)
pool chan net.Conn
maxSize int
activeConn int32
mu sync.RWMutex
}
func NewConnectionPool(factory func() (net.Conn, error), maxSize int) *ConnectionPool {
return &ConnectionPool{
factory: factory,
pool: make(chan net.Conn, maxSize),
maxSize: maxSize,
}
}
func (cp *ConnectionPool) Get() (net.Conn, error) {
select {
case conn := <-cp.pool:
// Test connection before returning
if err := testConnection(conn); err != nil {
conn.Close()
return cp.createConnection()
}
return conn, nil
default:
return cp.createConnection()
}
}
func (cp *ConnectionPool) Put(conn net.Conn) {
if conn == nil {
return
}
select {
case cp.pool <- conn:
// Successfully returned to pool
default:
// Pool full, close connection
conn.Close()
atomic.AddInt32(&cp.activeConn, -1)
}
}
func (cp *ConnectionPool) createConnection() (net.Conn, error) {
active := atomic.AddInt32(&cp.activeConn, 1)
if int(active) > cp.maxSize {
atomic.AddInt32(&cp.activeConn, -1)
return nil, errors.New("connection pool exhausted")
}
return cp.factory()
}
func testConnection(conn net.Conn) error {
// Set a short deadline for the test
conn.SetDeadline(time.Now().Add(time.Second))
defer conn.SetDeadline(time.Time{})
// Try to write and read a byte
if _, err := conn.Write([]byte{0}); err != nil {
return err
}
buffer := make([]byte, 1)
_, err := conn.Read(buffer)
return err
}
Anti-patterns to Avoid
The "God Goroutine" Anti-pattern
// ❌ Anti-pattern: Single goroutine doing everything
func godGoroutine(input <-chan Data) {
for data := range input {
// Validation
if !isValid(data) {
continue
}
// Transformation
transformed := transform(data)
// Database save
saveToDatabase(transformed)
// Send notification
sendNotification(transformed)
// Update cache
updateCache(transformed)
// Log metrics
logMetrics(transformed)
}
}
// ✅ Better: Separate concerns with pipelines
func processDataPipeline(input <-chan Data) {
validated := validateStage(input)
transformed := transformStage(validated)
// Fan out to multiple processing stages
go saveStage(transformed)
go notificationStage(transformed)
go cacheStage(transformed)
go metricsStage(transformed)
}
The "Premature Channels" Anti-pattern
// ❌ Anti-pattern: Unnecessary channel complexity
func unnecessaryChannels() {
ch1 := make(chan int)
ch2 := make(chan int)
ch3 := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch1 <- i
}
close(ch1)
}()
go func() {
for val := range ch1 {
ch2 <- val * 2
}
close(ch2)
}()
go func() {
for val := range ch2 {
ch3 <- val + 1
}
close(ch3)
}()
for result := range ch3 {
fmt.Println(result)
}
}
// ✅ Better: Simple sequential processing when no concurrency needed
func simpleProcessing() {
for i := 0; i < 10; i++ {
result := (i * 2) + 1
fmt.Println(result)
}
}
These performance patterns provide proven approaches to common optimization challenges in Go, helping you write efficient, scalable, and maintainable code while avoiding common pitfalls.