High-performance API development with Go
Table of Contents
- net/http Basics
- High-Performance Routers
- Zero-Allocation Patterns
- sync.Pool Usage
- JSON Optimization
- Connection Pooling
- Profiling with pprof
- Goroutine Management
- Context Timeouts
- Common Patterns
- 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")
// ...
})
Gin (Most Popular)
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