Introduction
Building microservices in Go requires more than just writing API handlers. You need to handle service discovery, fault tolerance, distributed tracing, and inter-service communication. This guide covers essential patterns for building production-ready microservices in Go.
What Are Microservices Patterns?
The Basic Concept
Microservices patterns are battle-tested solutions to common distributed systems challenges. They help you build resilient, scalable, and maintainable microservices architectures.
Key Terms
- Service Discovery: Automatically finding service instances
- Circuit Breaker: Preventing cascade failures
- Distributed Tracing: Tracking requests across services
- Event-Driven Architecture: Services communicating via events
- API Gateway: Single entry point for microservices
- Sidecar Pattern: Co-located helper containers
Why These Patterns Matter
| Challenge | Without Pattern | With Pattern |
|---|---|---|
| Service Failure | Cascade failure | Isolation |
| Finding Services | Hardcoded IPs | Auto-discovery |
| Debugging | Impossible | Full traceability |
| Scaling | Manual | Automatic |
1. Service Discovery
Registry Pattern
package discovery
import (
"fmt"
"sync"
"time"
)
type ServiceInstance struct {
ID string
Name string
Host string
Port int
HealthURL string
Metadata map[string]string
LastSeen time.Time
}
type Registry struct {
services map[string]map[string]*ServiceInstance
mu sync.RWMutex
}
func NewRegistry() *Registry {
return &Registry{
services: make(map[string]map[string]*ServiceInstance),
}
}
func (r *Registry) Register(instance *ServiceInstance) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.services[instance.Name]; !ok {
r.services[instance.Name] = make(map[string]*ServiceInstance)
}
instance.LastSeen = time.Now()
r.services[instance.Name][instance.ID] = instance
fmt.Printf("Registered: %s at %s:%d\n", instance.Name, instance.Host, instance.Port)
return nil
}
func (r *Registry) Deregister(serviceName, instanceID string) {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.services[serviceName]; ok {
delete(r.services[serviceName], instanceID)
}
}
func (r *Registry) GetInstances(serviceName string) []*ServiceInstance {
r.mu.RLock()
defer r.mu.RUnlock()
instances := make([]*ServiceInstance, 0)
if _, ok := r.services[serviceName]; ok {
for _, instance := range r.services[serviceName] {
instances = append(instances, instance)
}
}
return instances
}
func (r *Registry) StartHealthChecks(interval time.Duration) {
ticker := time.NewTicker(interval)
go func() {
for range ticker.C {
r.checkHealth()
}
}()
}
func (r *Registry) checkHealth() {
r.mu.Lock()
defer r.mu.Unlock()
for name, instances := range r.services {
for id, instance := range instances {
if time.Since(instance.LastSeen) > 30*time.Second {
delete(instances, id)
fmt.Printf("Deregistered: %s/%s (unhealthy)\n", name, id)
}
}
}
}
Client-Side Discovery
package discovery
import (
"fmt"
"math/rand"
"net/http"
)
type ServiceClient struct {
registry *Registry
serviceName string
}
func NewServiceClient(registry *Registry, serviceName string) *ServiceClient {
return &ServiceClient{
registry: registry,
serviceName: serviceName,
}
}
func (c *ServiceClient) DoRequest(method, path string, body []byte) (*http.Response, error) {
instances := c.registry.GetInstances(c.serviceName)
if len(instances) == 0 {
return nil, fmt.Errorf("no instances available for %s", c.serviceName)
}
instance := instances[rand.Intn(len(instances))]
url := fmt.Sprintf("http://%s:%d%s", instance.Host, instance.Port, path)
req, _ := http.NewRequest(method, url, nil)
return http.DefaultClient.Do(req)
}
2. Circuit Breaker
Implementation
package circuit
import (
"errors"
"fmt"
"sync"
"time"
)
var ErrCircuitOpen = errors.New("circuit breaker is open")
type CircuitState int
const (
StateClosed CircuitState = iota
StateOpen
StateHalfOpen
)
type CircuitBreaker struct {
name string
maxRequests int
interval time.Duration
timeout time.Duration
mu sync.Mutex
state CircuitState
failures int
successes int
lastFailure time.Time
nextRequest time.Time
}
func NewCircuitBreaker(name string, maxRequests int, interval, timeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
name: name,
maxRequests: maxRequests,
interval: interval,
timeout: timeout,
state: StateClosed,
}
}
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
cb.successes = 0
} 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.maxRequests {
cb.state = StateOpen
fmt.Printf("Circuit %s opened\n", cb.name)
}
return err
}
cb.successes++
cb.failures = 0
if cb.state == StateHalfOpen && cb.successes >= 2 {
cb.state = StateClosed
fmt.Printf("Circuit %s closed\n", cb.name)
}
return nil
}
func (cb *CircuitBreaker) GetState() CircuitState {
cb.mu.Lock()
defer cb.mu.Unlock()
return cb.state
}
3. Distributed Tracing
Trace Middleware
package tracing
import (
"fmt"
"math/rand"
"net/http"
"time"
)
type TraceID string
type SpanID string
type TraceContext struct {
TraceID TraceID
SpanID SpanID
Sampled bool
}
func generateTraceID() TraceID {
return TraceID(fmt.Sprintf("%016x", rand.Int63()))
}
func generateSpanID() SpanID {
return SpanID(fmt.Sprintf("%016x", rand.Int63()))
}
func TracingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
traceID := r.Header.Get("X-Trace-ID")
if traceID == "" {
traceID = string(generateTraceID())
}
spanID := generateSpanID()
ctx = WithTraceContext(ctx, &TraceContext{
TraceID: TraceID(traceID),
SpanID: spanID,
Sampled: true,
})
start := time.Now()
next.ServeHTTP(w, r.WithContext(ctx))
duration := time.Since(start)
fmt.Printf("[TRACE] %s %s - %v\n", r.Method, r.URL.Path, duration)
})
}
type contextKey string
const traceContextKey contextKey = "trace_context"
func WithTraceContext(ctx interface{ Context }, tc *TraceContext) interface {
Context
} {
return context.WithValue(ctx.Context(), traceContextKey, tc)
}
func GetTraceContext(ctx interface{ Context }) *TraceContext {
if tc, ok := ctx.Value(traceContextKey).(*TraceContext); ok {
return tc
}
return nil
}
4. Event-Driven Architecture
Event Bus
package events
import (
"encoding/json"
"fmt"
"sync"
"time"
)
type Event interface {
GetType() string
GetPayload() interface{}
}
type EventHandler func(Event) error
type EventBus struct {
handlers map[string][]EventHandler
mu sync.RWMutex
}
func NewEventBus() *EventBus {
return &EventBus{
handlers: make(map[string][]EventHandler),
}
}
func (eb *EventBus) Subscribe(eventType string, handler EventHandler) {
eb.mu.Lock()
defer eb.mu.Unlock()
eb.handlers[eventType] = append(eb.handlers[eventType], handler)
}
func (eb *EventBus) Publish(event Event) error {
eb.mu.RLock()
handlers := eb.handlers[event.GetType()]
eb.mu.RUnlock()
for _, handler := range handlers {
if err := handler(event); err != nil {
return err
}
}
return nil
}
type OrderCreatedEvent struct {
Type string `json:"type"`
Timestamp time.Time `json:"timestamp"`
Payload OrderPayload `json:"payload"`
}
type OrderPayload struct {
OrderID string `json:"order_id"`
CustomerID string `json:"customer_id"`
Total float64 `json:"total"`
}
func (e OrderCreatedEvent) GetType() string { return e.Type }
func (e OrderCreatedEvent) GetPayload() interface{} { return e.Payload }
func main() {
bus := NewEventBus()
bus.Subscribe("order.created", func(e Event) error {
payload := e.GetPayload().(OrderPayload)
fmt.Printf("Processing order: %s - $%.2f\n", payload.OrderID, payload.Total)
return nil
})
event := OrderCreatedEvent{
Type: "order.created",
Timestamp: time.Now(),
Payload: OrderPayload{
OrderID: "ORD-001",
CustomerID: "CUST-123",
Total: 99.99,
},
}
bus.Publish(event)
}
5. API Gateway
package gateway
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"strings"
)
type Route struct {
Path string
Methods []string
Backend string
}
type APIGateway struct {
routes []Route
}
func NewAPIGateway() *APIGateway {
return &APIGateway{
routes: []Route{
{Path: "/api/users", Backend: "user-service:8081"},
{Path: "/api/orders", Backend: "order-service:8082"},
{Path: "/api/products", Backend: "product-service:8083"},
},
}
}
func (gw *APIGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
route := gw.findRoute(r.URL.Path)
if route == nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
gw.proxyRequest(w, r, route.Backend)
}
func (gw *APIGateway) findRoute(path string) *Route {
for _, route := range gw.routes {
if strings.HasPrefix(path, route.Path) {
return &route
}
}
return nil
}
func (gw *APIGateway) proxyRequest(w http.ResponseWriter, r *http.Request, backend string) {
url := fmt.Sprintf("http://%s%s", backend, r.URL.Path)
req, _ := http.NewRequest(r.Method, url, r.Body)
req.Header = r.Header.Clone()
resp, err := http.DefaultClient.Do(req)
if err != nil {
http.Error(w, "Bad Gateway", http.StatusBadGateway)
return
}
defer resp.Body.Close()
w.WriteHeader(resp.StatusCode)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
type LoadBalancer struct {
backends map[string][]string
}
func NewLoadBalancer() *LoadBalancer {
return &LoadBalancer{
backends: make(map[string][]string),
}
}
func (lb *LoadBalancer) GetBackend(service string) string {
backends := lb.backends[service]
if len(backends) == 0 {
return ""
}
return backends[rand.Intn(len(backends))]
}
Best Practices
1. Graceful Shutdown
func main() {
server := &http.Server{Addr: ":8080", Handler: router}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(ctx)
}
2. Health Checks
func healthCheck(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
if err := checkDependencies(ctx); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]string{"status": "unhealthy"})
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
}
3. Context Propagation
func callDownstreamService(ctx context.Context, url string) (*http.Response, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
return http.DefaultClient.Do(req)
}
External Resources
Tools
- Go Kit - Microservices toolkit
- gRPC - High-performance RPC
- Consul - Service discovery
- Jaeger - Distributed tracing
Learning
Key Takeaways
- Service Discovery enables dynamic service registration
- Circuit Breaker prevents cascade failures
- Distributed Tracing provides visibility across services
- Event-Driven architecture decouples services
- API Gateway provides unified entry point
- Best Practices: graceful shutdown, health checks, context propagation
Comments