Go Microservices Best Practices¶
Objective: Master senior-level Go microservices patterns for production systems. When you need to build robust, scalable distributed systems, when you want to follow proven methodologies, when you need enterprise-grade microservices patternsโthese best practices become your weapon of choice.
Core Principles¶
- Service Independence: Services should be independently deployable and scalable
- Fault Tolerance: Design for failure and implement circuit breakers
- Observability: Comprehensive logging, metrics, and tracing
- API-First Design: Well-defined service interfaces
- Data Consistency: Handle distributed data challenges
Service Architecture¶
Service Structure¶
// internal/service/service.go
package service
import (
"context"
"fmt"
"time"
)
// Service represents a microservice
type Service struct {
name string
version string
port int
dependencies []Dependency
healthCheck HealthCheck
metrics Metrics
logger Logger
}
// Dependency represents a service dependency
type Dependency struct {
Name string
URL string
Timeout time.Duration
Retries int
}
// HealthCheck represents a health check
type HealthCheck struct {
Endpoint string
Interval time.Duration
Timeout time.Duration
}
// NewService creates a new service
func NewService(name, version string, port int) *Service {
return &Service{
name: name,
version: version,
port: port,
dependencies: make([]Dependency, 0),
healthCheck: HealthCheck{
Endpoint: "/health",
Interval: 30 * time.Second,
Timeout: 5 * time.Second,
},
}
}
// AddDependency adds a service dependency
func (s *Service) AddDependency(name, url string, timeout time.Duration, retries int) {
s.dependencies = append(s.dependencies, Dependency{
Name: name,
URL: url,
Timeout: timeout,
Retries: retries,
})
}
// Start starts the service
func (s *Service) Start(ctx context.Context) error {
s.logger.Info("Starting service", map[string]interface{}{
"name": s.name,
"version": s.version,
"port": s.port,
})
// Start health check
go s.startHealthCheck(ctx)
// Start metrics collection
go s.startMetricsCollection(ctx)
return nil
}
// startHealthCheck starts the health check routine
func (s *Service) startHealthCheck(ctx context.Context) {
ticker := time.NewTicker(s.healthCheck.Interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
s.performHealthCheck()
}
}
}
// performHealthCheck performs a health check
func (s *Service) performHealthCheck() {
for _, dep := range s.dependencies {
if err := s.checkDependency(dep); err != nil {
s.logger.Error("Dependency health check failed", map[string]interface{}{
"dependency": dep.Name,
"error": err.Error(),
})
}
}
}
// checkDependency checks a dependency's health
func (s *Service) checkDependency(dep Dependency) error {
ctx, cancel := context.WithTimeout(context.Background(), dep.Timeout)
defer cancel()
// Implement health check logic
return s.pingDependency(ctx, dep.URL)
}
// pingDependency pings a dependency
func (s *Service) pingDependency(ctx context.Context, url string) error {
// Implement ping logic
return nil
}
Service Discovery¶
// internal/discovery/discovery.go
package discovery
import (
"context"
"fmt"
"sync"
"time"
)
// ServiceRegistry represents a service registry
type ServiceRegistry struct {
services map[string][]ServiceInstance
mutex sync.RWMutex
ttl time.Duration
}
// ServiceInstance represents a service instance
type ServiceInstance struct {
ID string
Name string
Address string
Port int
Metadata map[string]string
TTL time.Time
}
// NewServiceRegistry creates a new service registry
func NewServiceRegistry(ttl time.Duration) *ServiceRegistry {
return &ServiceRegistry{
services: make(map[string][]ServiceInstance),
ttl: ttl,
}
}
// Register registers a service instance
func (sr *ServiceRegistry) Register(instance ServiceInstance) error {
sr.mutex.Lock()
defer sr.mutex.Unlock()
instance.TTL = time.Now().Add(sr.ttl)
sr.services[instance.Name] = append(sr.services[instance.Name], instance)
return nil
}
// Deregister deregisters a service instance
func (sr *ServiceRegistry) Deregister(serviceName, instanceID string) error {
sr.mutex.Lock()
defer sr.mutex.Unlock()
instances, exists := sr.services[serviceName]
if !exists {
return fmt.Errorf("service %s not found", serviceName)
}
for i, instance := range instances {
if instance.ID == instanceID {
sr.services[serviceName] = append(instances[:i], instances[i+1:]...)
return nil
}
}
return fmt.Errorf("instance %s not found", instanceID)
}
// Discover discovers service instances
func (sr *ServiceRegistry) Discover(serviceName string) ([]ServiceInstance, error) {
sr.mutex.RLock()
defer sr.mutex.RUnlock()
instances, exists := sr.services[serviceName]
if !exists {
return nil, fmt.Errorf("service %s not found", serviceName)
}
// Filter expired instances
var validInstances []ServiceInstance
for _, instance := range instances {
if time.Now().Before(instance.TTL) {
validInstances = append(validInstances, instance)
}
}
return validInstances, nil
}
// Cleanup removes expired instances
func (sr *ServiceRegistry) Cleanup() {
sr.mutex.Lock()
defer sr.mutex.Unlock()
for serviceName, instances := range sr.services {
var validInstances []ServiceInstance
for _, instance := range instances {
if time.Now().Before(instance.TTL) {
validInstances = append(validInstances, instance)
}
}
sr.services[serviceName] = validInstances
}
}
Inter-Service Communication¶
HTTP Client¶
// internal/client/http_client.go
package client
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// HTTPClient represents an HTTP client
type HTTPClient struct {
baseURL string
timeout time.Duration
retries int
circuitBreaker *CircuitBreaker
logger Logger
}
// NewHTTPClient creates a new HTTP client
func NewHTTPClient(baseURL string, timeout time.Duration, retries int) *HTTPClient {
return &HTTPClient{
baseURL: baseURL,
timeout: timeout,
retries: retries,
circuitBreaker: NewCircuitBreaker(5, time.Minute),
logger: NewLogger(),
}
}
// Get performs a GET request
func (c *HTTPClient) Get(ctx context.Context, path string, result interface{}) error {
return c.doRequest(ctx, "GET", path, nil, result)
}
// Post performs a POST request
func (c *HTTPClient) Post(ctx context.Context, path string, data interface{}, result interface{}) error {
return c.doRequest(ctx, "POST", path, data, result)
}
// Put performs a PUT request
func (c *HTTPClient) Put(ctx context.Context, path string, data interface{}, result interface{}) error {
return c.doRequest(ctx, "PUT", path, data, result)
}
// Delete performs a DELETE request
func (c *HTTPClient) Delete(ctx context.Context, path string) error {
return c.doRequest(ctx, "DELETE", path, nil, nil)
}
// doRequest performs an HTTP request
func (c *HTTPClient) doRequest(ctx context.Context, method, path string, data interface{}, result interface{}) error {
return c.circuitBreaker.Execute(ctx, func() error {
return c.doRequestWithRetry(ctx, method, path, data, result)
})
}
// doRequestWithRetry performs a request with retry logic
func (c *HTTPClient) doRequestWithRetry(ctx context.Context, method, path string, data interface{}, result interface{}) error {
var lastErr error
for i := 0; i <= c.retries; i++ {
if i > 0 {
time.Sleep(time.Duration(i) * time.Second)
}
err := c.performRequest(ctx, method, path, data, result)
if err == nil {
return nil
}
lastErr = err
c.logger.Warn("Request failed, retrying", map[string]interface{}{
"method": method,
"path": path,
"attempt": i + 1,
"error": err.Error(),
})
}
return fmt.Errorf("request failed after %d retries: %w", c.retries, lastErr)
}
// performRequest performs a single HTTP request
func (c *HTTPClient) performRequest(ctx context.Context, method, path string, data interface{}, result interface{}) error {
url := c.baseURL + path
var body []byte
var err error
if data != nil {
body, err = json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
}
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "microservice-client/1.0")
client := &http.Client{Timeout: c.timeout}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to perform request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("request failed with status %d", resp.StatusCode)
}
if result != nil {
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
}
return nil
}
gRPC Client¶
// internal/client/grpc_client.go
package client
import (
"context"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// GRPCClient represents a gRPC client
type GRPCClient struct {
conn *grpc.ClientConn
logger Logger
}
// NewGRPCClient creates a new gRPC client
func NewGRPCClient(address string) (*GRPCClient, error) {
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, fmt.Errorf("failed to connect to gRPC server: %w", err)
}
return &GRPCClient{
conn: conn,
logger: NewLogger(),
}, nil
}
// Close closes the gRPC connection
func (c *GRPCClient) Close() error {
return c.conn.Close()
}
// Call performs a gRPC call
func (c *GRPCClient) Call(ctx context.Context, method string, req, resp interface{}) error {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
return grpc.Invoke(ctx, method, req, resp, c.conn)
}
Circuit Breaker Pattern¶
Circuit Breaker Implementation¶
// internal/circuitbreaker/circuitbreaker.go
package circuitbreaker
import (
"context"
"errors"
"sync"
"time"
)
// CircuitState represents the state of the circuit breaker
type CircuitState int
const (
StateClosed CircuitState = iota
StateOpen
StateHalfOpen
)
// CircuitBreaker implements the circuit breaker pattern
type CircuitBreaker struct {
mu sync.RWMutex
state CircuitState
failures int
threshold int
timeout time.Duration
lastFailure time.Time
successCount int
halfOpenMaxCalls int
}
// NewCircuitBreaker creates a new circuit breaker
func NewCircuitBreaker(threshold int, timeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
state: StateClosed,
threshold: threshold,
timeout: timeout,
halfOpenMaxCalls: 3,
}
}
// Execute executes a function with circuit breaker protection
func (cb *CircuitBreaker) Execute(ctx context.Context, fn func() error) error {
if !cb.allowRequest() {
return errors.New("circuit breaker is open")
}
err := fn()
cb.recordResult(err)
return err
}
// allowRequest checks if a request should be allowed
func (cb *CircuitBreaker) allowRequest() bool {
cb.mu.RLock()
defer cb.mu.RUnlock()
switch cb.state {
case StateClosed:
return true
case StateOpen:
return time.Since(cb.lastFailure) > cb.timeout
case StateHalfOpen:
return cb.successCount < cb.halfOpenMaxCalls
default:
return false
}
}
// recordResult records the result of a request
func (cb *CircuitBreaker) recordResult(err error) {
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
cb.failures++
cb.lastFailure = time.Now()
if cb.failures >= cb.threshold {
cb.state = StateOpen
}
} else {
cb.failures = 0
cb.successCount++
if cb.state == StateHalfOpen && cb.successCount >= cb.halfOpenMaxCalls {
cb.state = StateClosed
cb.successCount = 0
}
}
}
// GetState returns the current state
func (cb *CircuitBreaker) GetState() CircuitState {
cb.mu.RLock()
defer cb.mu.RUnlock()
return cb.state
}
Load Balancing¶
Load Balancer¶
// internal/loadbalancer/loadbalancer.go
package loadbalancer
import (
"context"
"math/rand"
"sync"
"time"
)
// LoadBalancer represents a load balancer
type LoadBalancer struct {
instances []ServiceInstance
strategy LoadBalanceStrategy
mutex sync.RWMutex
}
// LoadBalanceStrategy represents a load balancing strategy
type LoadBalanceStrategy int
const (
StrategyRoundRobin LoadBalanceStrategy = iota
StrategyRandom
StrategyLeastConnections
)
// NewLoadBalancer creates a new load balancer
func NewLoadBalancer(strategy LoadBalanceStrategy) *LoadBalancer {
return &LoadBalancer{
instances: make([]ServiceInstance, 0),
strategy: strategy,
}
}
// AddInstance adds a service instance
func (lb *LoadBalancer) AddInstance(instance ServiceInstance) {
lb.mutex.Lock()
defer lb.mutex.Unlock()
lb.instances = append(lb.instances, instance)
}
// RemoveInstance removes a service instance
func (lb *LoadBalancer) RemoveInstance(instanceID string) {
lb.mutex.Lock()
defer lb.mutex.Unlock()
for i, instance := range lb.instances {
if instance.ID == instanceID {
lb.instances = append(lb.instances[:i], lb.instances[i+1:]...)
break
}
}
}
// GetInstance returns a service instance
func (lb *LoadBalancer) GetInstance() (ServiceInstance, error) {
lb.mutex.RLock()
defer lb.mutex.RUnlock()
if len(lb.instances) == 0 {
return ServiceInstance{}, errors.New("no instances available")
}
switch lb.strategy {
case StrategyRoundRobin:
return lb.getRoundRobinInstance()
case StrategyRandom:
return lb.getRandomInstance()
case StrategyLeastConnections:
return lb.getLeastConnectionsInstance()
default:
return lb.getRoundRobinInstance()
}
}
// getRoundRobinInstance returns an instance using round-robin
func (lb *LoadBalancer) getRoundRobinInstance() ServiceInstance {
// Implement round-robin logic
return lb.instances[0]
}
// getRandomInstance returns a random instance
func (lb *LoadBalancer) getRandomInstance() ServiceInstance {
rand.Seed(time.Now().UnixNano())
return lb.instances[rand.Intn(len(lb.instances))]
}
// getLeastConnectionsInstance returns the instance with least connections
func (lb *LoadBalancer) getLeastConnectionsInstance() ServiceInstance {
// Implement least connections logic
return lb.instances[0]
}
Event-Driven Architecture¶
Event Bus¶
// internal/events/event_bus.go
package events
import (
"context"
"sync"
"time"
)
// Event represents an event
type Event struct {
ID string
Type string
Source string
Data interface{}
Timestamp time.Time
Version string
}
// EventHandler represents an event handler
type EventHandler interface {
Handle(ctx context.Context, event Event) error
}
// EventBus represents an event bus
type EventBus struct {
handlers map[string][]EventHandler
mutex sync.RWMutex
logger Logger
}
// NewEventBus creates a new event bus
func NewEventBus() *EventBus {
return &EventBus{
handlers: make(map[string][]EventHandler),
logger: NewLogger(),
}
}
// Subscribe subscribes to an event type
func (eb *EventBus) Subscribe(eventType string, handler EventHandler) {
eb.mutex.Lock()
defer eb.mutex.Unlock()
eb.handlers[eventType] = append(eb.handlers[eventType], handler)
}
// Publish publishes an event
func (eb *EventBus) Publish(ctx context.Context, event Event) error {
eb.mutex.RLock()
handlers := eb.handlers[event.Type]
eb.mutex.RUnlock()
for _, handler := range handlers {
go func(h EventHandler) {
if err := h.Handle(ctx, event); err != nil {
eb.logger.Error("Event handler failed", map[string]interface{}{
"event_type": event.Type,
"error": err.Error(),
})
}
}(handler)
}
return nil
}
Message Queue¶
// internal/messaging/queue.go
package messaging
import (
"context"
"encoding/json"
"time"
)
// Message represents a message
type Message struct {
ID string
Type string
Data interface{}
Timestamp time.Time
Retries int
MaxRetries int
}
// MessageQueue represents a message queue
type MessageQueue struct {
name string
handler MessageHandler
logger Logger
}
// MessageHandler represents a message handler
type MessageHandler interface {
Handle(ctx context.Context, message Message) error
}
// NewMessageQueue creates a new message queue
func NewMessageQueue(name string, handler MessageHandler) *MessageQueue {
return &MessageQueue{
name: name,
handler: handler,
logger: NewLogger(),
}
}
// Publish publishes a message
func (mq *MessageQueue) Publish(ctx context.Context, messageType string, data interface{}) error {
message := Message{
ID: generateID(),
Type: messageType,
Data: data,
Timestamp: time.Now(),
Retries: 0,
MaxRetries: 3,
}
return mq.publishMessage(ctx, message)
}
// publishMessage publishes a message to the queue
func (mq *MessageQueue) publishMessage(ctx context.Context, message Message) error {
// Implement message publishing logic
return nil
}
// Consume consumes messages from the queue
func (mq *MessageQueue) Consume(ctx context.Context) error {
// Implement message consumption logic
return nil
}
// generateID generates a unique ID
func generateID() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
Distributed Tracing¶
Tracing Implementation¶
// internal/tracing/tracing.go
package tracing
import (
"context"
"fmt"
"time"
)
// Trace represents a distributed trace
type Trace struct {
ID string
ParentID string
Operation string
StartTime time.Time
EndTime time.Time
Tags map[string]string
Logs []Log
}
// Log represents a trace log
type Log struct {
Timestamp time.Time
Fields map[string]interface{}
}
// Tracer represents a tracer
type Tracer struct {
serviceName string
logger Logger
}
// NewTracer creates a new tracer
func NewTracer(serviceName string) *Tracer {
return &Tracer{
serviceName: serviceName,
logger: NewLogger(),
}
}
// StartSpan starts a new span
func (t *Tracer) StartSpan(ctx context.Context, operation string) (context.Context, *Span) {
span := &Span{
trace: t,
operation: operation,
startTime: time.Now(),
tags: make(map[string]string),
logs: make([]Log, 0),
}
return context.WithValue(ctx, "span", span), span
}
// Span represents a span
type Span struct {
trace *Tracer
operation string
startTime time.Time
endTime time.Time
tags map[string]string
logs []Log
}
// SetTag sets a tag on the span
func (s *Span) SetTag(key, value string) {
s.tags[key] = value
}
// Log logs an event on the span
func (s *Span) Log(fields map[string]interface{}) {
s.logs = append(s.logs, Log{
Timestamp: time.Now(),
Fields: fields,
})
}
// Finish finishes the span
func (s *Span) Finish() {
s.endTime = time.Now()
trace := Trace{
Operation: s.operation,
StartTime: s.startTime,
EndTime: s.endTime,
Tags: s.tags,
Logs: s.logs,
}
s.trace.logger.Info("Span finished", map[string]interface{}{
"operation": s.operation,
"duration": s.endTime.Sub(s.startTime),
"tags": s.tags,
})
}
Service Mesh Integration¶
Service Mesh Client¶
// internal/mesh/mesh_client.go
package mesh
import (
"context"
"fmt"
"time"
)
// MeshClient represents a service mesh client
type MeshClient struct {
serviceName string
namespace string
logger Logger
}
// NewMeshClient creates a new service mesh client
func NewMeshClient(serviceName, namespace string) *MeshClient {
return &MeshClient{
serviceName: serviceName,
namespace: namespace,
logger: NewLogger(),
}
}
// CallService calls a service through the mesh
func (mc *MeshClient) CallService(ctx context.Context, serviceName, method string, data interface{}) (interface{}, error) {
start := time.Now()
// Implement service mesh call logic
result, err := mc.performMeshCall(ctx, serviceName, method, data)
duration := time.Since(start)
mc.logger.Info("Service mesh call completed", map[string]interface{}{
"service": serviceName,
"method": method,
"duration": duration,
"error": err != nil,
})
return result, err
}
// performMeshCall performs a call through the service mesh
func (mc *MeshClient) performMeshCall(ctx context.Context, serviceName, method string, data interface{}) (interface{}, error) {
// Implement mesh call logic
return nil, fmt.Errorf("mesh call not implemented")
}
Testing Microservices¶
Integration Testing¶
// internal/testing/integration_test.go
package testing
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMicroservice_Integration(t *testing.T) {
// Arrange
service := NewService("test-service", "1.0.0", 8080)
service.AddDependency("user-service", "http://localhost:8081", 5*time.Second, 3)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Act
err := service.Start(ctx)
// Assert
require.NoError(t, err)
// Test service functionality
time.Sleep(100 * time.Millisecond)
// Verify service is running
assert.True(t, service.IsHealthy())
}
func TestCircuitBreaker_Integration(t *testing.T) {
// Arrange
cb := NewCircuitBreaker(3, time.Minute)
// Act & Assert
// Test circuit breaker behavior
for i := 0; i < 5; i++ {
err := cb.Execute(context.Background(), func() error {
return fmt.Errorf("test error")
})
if i < 3 {
assert.Error(t, err)
} else {
assert.Error(t, err)
assert.Contains(t, err.Error(), "circuit breaker is open")
}
}
}
TL;DR Runbook¶
Quick Start¶
// 1. Create a microservice
service := NewService("user-service", "1.0.0", 8080)
service.AddDependency("database", "postgres://localhost:5432", 5*time.Second, 3)
// 2. Start the service
ctx := context.Background()
service.Start(ctx)
// 3. Circuit breaker
cb := NewCircuitBreaker(5, time.Minute)
err := cb.Execute(ctx, func() error {
return callExternalService()
})
// 4. Load balancer
lb := NewLoadBalancer(StrategyRoundRobin)
lb.AddInstance(ServiceInstance{ID: "1", Address: "localhost:8081"})
instance, err := lb.GetInstance()
Essential Patterns¶
// Service discovery
registry := NewServiceRegistry(30 * time.Second)
registry.Register(ServiceInstance{
ID: "user-service-1",
Name: "user-service",
Address: "localhost:8080",
Port: 8080,
})
// Event-driven communication
eventBus := NewEventBus()
eventBus.Subscribe("user.created", &UserCreatedHandler{})
eventBus.Publish(ctx, Event{
Type: "user.created",
Data: userData,
})
// Distributed tracing
tracer := NewTracer("user-service")
ctx, span := tracer.StartSpan(ctx, "create-user")
defer span.Finish()
This guide provides the complete machinery for building production-ready microservices in Go applications. Each pattern includes implementation examples, testing strategies, and real-world usage patterns for enterprise deployment.