Back to Articles
Go API Performance: Routers, Pooling, Zero Allocation

Go API Performance: Routers, Pooling, Zero Allocation

High-performance API development with Go


Table of Contents

  1. net/http Basics
  2. High-Performance Routers
  3. Zero-Allocation Patterns
  4. sync.Pool Usage
  5. JSON Optimization
  6. Connection Pooling
  7. Profiling with pprof
  8. Goroutine Management
  9. Context Timeouts
  10. Common Patterns
  11. Quick Reference

1. net/http Basics

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/api/hello", helloHandler)

    server := &http.Server{
        Addr:         ":8080",
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    log.Fatal(server.ListenAndServe())
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"message": "Hello"})
}

2. High-Performance Routers

Chi (Lightweight, Idiomatic)

import "github.com/go-chi/chi/v5"

r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)

r.Get("/api/users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    // ...
})
import "github.com/gin-gonic/gin"

gin.SetMode(gin.ReleaseMode)  // Production mode
r := gin.New()
r.Use(gin.Recovery())

r.GET("/api/users/:id", func(c *gin.Context) {
    id := c.Param("id")
    c.JSON(200, gin.H{"id": id})
})

Fiber (fasthttp-based)

import "github.com/gofiber/fiber/v2"

app := fiber.New(fiber.Config{
    Prefork:       true,  // Multiple processes
    ServerHeader:  "",    // Remove header
    StrictRouting: true,
})

app.Get("/api/users/:id", func(c *fiber.Ctx) error {
    id := c.Params("id")
    return c.JSON(fiber.Map{"id": id})
})

Performance Comparison:

Fiber (fasthttp): ~400,000 req/s
Gin:              ~150,000 req/s
Chi:              ~100,000 req/s
net/http:         ~80,000 req/s

3. Zero-Allocation Patterns

// BAD: Allocates on every request
func badHandler(w http.ResponseWriter, r *http.Request) {
    data := map[string]string{"message": "Hello"}  // Allocation!
    json.NewEncoder(w).Encode(data)  // Allocation!
}

// GOOD: Reuse with sync.Pool
var responsePool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

type Response struct {
    Message string `json:"message"`
}

var responseTemplate = Response{Message: "Hello"}

func goodHandler(w http.ResponseWriter, r *http.Request) {
    buf := responsePool.Get().(*bytes.Buffer)
    buf.Reset()
    defer responsePool.Put(buf)

    json.NewEncoder(buf).Encode(responseTemplate)
    w.Header().Set("Content-Type", "application/json")
    w.Write(buf.Bytes())
}

4. sync.Pool Usage

// Pool for request contexts
type RequestContext struct {
    UserID    int64
    RequestID string
    StartTime time.Time
    Buffer    []byte
}

var ctxPool = sync.Pool{
    New: func() interface{} {
        return &RequestContext{
            Buffer: make([]byte, 4096),
        }
    },
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
    ctx := ctxPool.Get().(*RequestContext)
    defer func() {
        ctx.UserID = 0
        ctx.RequestID = ""
        ctx.Buffer = ctx.Buffer[:0]
        ctxPool.Put(ctx)
    }()

    ctx.StartTime = time.Now()
    ctx.RequestID = generateRequestID()

    // Use ctx.Buffer for temporary data
    // ...
}

5. JSON Optimization

Standard Library (Safe, Slower)

import "encoding/json"

// BAD: Creates encoder each time
func slowJSON(w http.ResponseWriter, data interface{}) {
    json.NewEncoder(w).Encode(data)
}

Sonic/go-json (2-5x Faster)

import "github.com/bytedance/sonic"

func fastJSON(w http.ResponseWriter, data interface{}) {
    bytes, _ := sonic.Marshal(data)
    w.Write(bytes)
}

Pre-marshal Static Responses

var healthResponse = mustMarshal(map[string]string{"status": "ok"})

func mustMarshal(v interface{}) []byte {
    b, err := json.Marshal(v)
    if err != nil {
        panic(err)
    }
    return b
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write(healthResponse)  // Zero allocation
}

6. Connection Pooling

Database

import (
    "database/sql"
    _ "github.com/lib/pq"
)

func initDB() *sql.DB {
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        log.Fatal(err)
    }

    // Connection pool settings
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(25)
    db.SetConnMaxLifetime(5 * time.Minute)
    db.SetConnMaxIdleTime(5 * time.Minute)

    // Verify connection
    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }

    return db
}

HTTP Client

var httpClient = &http.Client{
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 100,
        MaxConnsPerHost:     100,
        IdleConnTimeout:     90 * time.Second,
    },
    Timeout: 10 * time.Second,
}

7. Profiling with pprof

Setup

import (
    "net/http"
    _ "net/http/pprof"
)

func main() {
    // pprof endpoints: /debug/pprof/
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    // Main server
    // ...
}

Commands

# CPU profile
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30

# Memory profile
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap

# Allocation analysis
go tool pprof --alloc_objects http://localhost:6060/debug/pprof/allocs

# Trace
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5
go tool trace trace.out

8. Goroutine Management

Semaphore Pattern

type Semaphore chan struct{}

func NewSemaphore(max int) Semaphore {
    return make(chan struct{}, max)
}

func (s Semaphore) Acquire() {
    s <- struct{}{}
}

func (s Semaphore) Release() {
    <-s
}

Worker Pool

type WorkerPool struct {
    jobs    chan Job
    results chan Result
    workers int
}

func NewWorkerPool(workers, queueSize int) *WorkerPool {
    p := &WorkerPool{
        jobs:    make(chan Job, queueSize),
        results: make(chan Result, queueSize),
        workers: workers,
    }

    for i := 0; i < workers; i++ {
        go p.worker()
    }

    return p
}

func (p *WorkerPool) worker() {
    for job := range p.jobs {
        result := processJob(job)
        p.results <- result
    }
}

9. Context Timeouts

func handleWithTimeout(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()

    // Pass context to database queries
    row := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)

    // Pass context to HTTP requests
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := httpClient.Do(req)

    // Check for timeout
    if ctx.Err() == context.DeadlineExceeded {
        http.Error(w, "Request timeout", http.StatusGatewayTimeout)
        return
    }
}

10. Common Patterns

Rate Limiting

import "golang.org/x/time/rate"

limiter := rate.NewLimiter(rate.Limit(100), 10)  // 100 req/s, burst 10

func rateLimitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !limiter.Allow() {
            http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

Circuit Breaker

import "github.com/sony/gobreaker"

cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
    Name:        "external-api",
    MaxRequests: 3,
    Interval:    10 * time.Second,
    Timeout:     30 * time.Second,
    ReadyToTrip: func(counts gobreaker.Counts) bool {
        return counts.ConsecutiveFailures > 5
    },
})

func callExternalAPI() (interface{}, error) {
    return cb.Execute(func() (interface{}, error) {
        return http.Get("https://external.api/endpoint")
    })
}

Graceful Shutdown

func main() {
    server := &http.Server{Addr: ":8080"}

    go func() {
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatal(err)
    }

    log.Println("Server stopped gracefully")
}

Request Coalescing

import "golang.org/x/sync/singleflight"

var group singleflight.Group

func getUser(id string) (*User, error) {
    result, err, _ := group.Do(id, func() (interface{}, error) {
        // Only one request to DB for concurrent calls with same ID
        return db.GetUser(id)
    })

    if err != nil {
        return nil, err
    }
    return result.(*User), nil
}

11. Quick Reference

Optimization Checklist

GO PERFORMANCE:
├── [ ] Use Gin or Fiber for high throughput
├── [ ] Implement sync.Pool for hot path objects
├── [ ] Use sonic/go-json for faster JSON
├── [ ] Configure connection pool for database
├── [ ] Reuse http.Client instances
├── [ ] Profile with pprof
├── [ ] Implement graceful shutdown
├── [ ] Use context timeouts for all I/O
├── [ ] Pre-marshal static responses
└── [ ] Use singleflight for request coalescing

10ms Latency Budget

GO (Budget: 10ms total):
├── Routing:         0.05ms
├── Middleware:      0.5ms
├── Database:        5ms
├── Business logic:  2ms
├── JSON encoding:   0.5ms
├── Response:        1.95ms

Memory Profiling Tips

COMMON ALLOCATIONS:
├── Map/slice creation in hot paths
├── String concatenation
├── Interface boxing
├── JSON encoding/decoding
└── HTTP response body reads

SOLUTIONS:
├── sync.Pool for reusable objects
├── bytes.Buffer for string building
├── Pre-allocate slices with capacity
├── Use sonic for JSON
└── io.Copy for streaming

Benchmarking

# wrk - HTTP benchmarking
wrk -t12 -c400 -d30s http://localhost:8080/api/hello

# hey - Go-based HTTP load generator
hey -n 10000 -c 100 http://localhost:8080/api/hello

# k6 - Modern load testing
k6 run --vus 100 --duration 30s script.js