When building distributed systems at scale, Go has become my go-to language. Its concurrency model, small memory footprint, and fast compilation make it ideal for microservices that need to handle thousands of requests per second.
Why Go for Microservices?
Go was designed at Google to solve real-world problems in large-scale distributed systems. Here's what makes it stand out:
- Goroutines: Lightweight threads that cost ~2KB of stack space
- Channels: First-class communication primitives for safe concurrency
- Static binary: Single binary deployment with no runtime dependencies
- Fast GC: Sub-millisecond garbage collection pauses
Architecture Overview
Here's the architecture I typically use for production microservices:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
type Server struct {
router *http.ServeMux
httpServer *http.Server
logger *log.Logger
}
func NewServer(addr string) *Server {
s := &Server{
router: http.NewServeMux(),
logger: log.New(os.Stdout, "[service] ", log.LstdFlags),
}
s.httpServer = &http.Server{
Addr: addr,
Handler: s.middleware(s.router),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
s.routes()
return s
}
func (s *Server) middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
s.logger.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}Implementing Circuit Breakers
One of the most critical patterns in microservice architecture is the circuit breaker. It prevents cascading failures when downstream services are unhealthy.
type CircuitBreaker struct {
mu sync.Mutex
state State
failures int
threshold int
timeout time.Duration
lastFailure time.Time
}
type State int
const (
StateClosed State = iota
StateOpen
StateHalfOpen
)
func (cb *CircuitBreaker) Execute(fn func() error) error {
cb.mu.Lock()
if cb.state == StateOpen {
if time.Since(cb.lastFailure) > cb.timeout {
cb.state = StateHalfOpen
} else {
cb.mu.Unlock()
return ErrCircuitOpen
}
}
cb.mu.Unlock()
err := fn()
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
cb.failures++
cb.lastFailure = time.Now()
if cb.failures >= cb.threshold {
cb.state = StateOpen
}
return err
}
cb.failures = 0
cb.state = StateClosed
return nil
}Rate Limiting with Token Bucket
For API rate limiting, I use a token bucket implementation that's both memory-efficient and fair:
type TokenBucket struct {
rate float64
capacity float64
tokens float64
lastRefill time.Time
mu sync.Mutex
}
func NewTokenBucket(rate, capacity float64) *TokenBucket {
return &TokenBucket{
rate: rate,
capacity: capacity,
tokens: capacity,
lastRefill: time.Now(),
}
}
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
now := time.Now()
elapsed := now.Sub(tb.lastRefill).Seconds()
tb.tokens = math.Min(tb.capacity, tb.tokens+elapsed*tb.rate)
tb.lastRefill = now
if tb.tokens >= 1 {
tb.tokens--
return true
}
return false
}Graceful Shutdown
Always implement graceful shutdown. In-flight requests should complete before the service terminates:
func main() {
srv := NewServer(":8080")
go func() {
if err := srv.httpServer.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("HTTP server error: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// Give outstanding requests 30 seconds to complete
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.httpServer.Shutdown(ctx); err != nil {
log.Fatalf("Forced shutdown: %v", err)
}
log.Println("Server stopped gracefully")
}Key Takeaways
- Design for failure: Every network call can fail. Plan for it.
- Observe everything: Metrics, logs, and traces are non-negotiable.
- Keep services small: Each service should do one thing well.
- Automate deployment: CI/CD pipelines should handle the boring stuff.
The beauty of Go is that it forces you to handle errors explicitly. There's no hiding behind try-catch blocks. Every error is a conscious decision.
Building resilient systems is a journey, not a destination. Keep iterating.