Why Go for Microservices?

Go is practically built for microservices:

  • Small binaries — typically 10-20MB, perfect for containers
  • Fast startup — milliseconds, not seconds
  • Built-in concurrency — goroutines handle thousands of connections
  • Simple deployment — single binary, no runtime dependencies
  • Excellent tooling — gRPC, Docker, K8s all have first-class Go support

Architecture Overview

We'll build a simple e-commerce system with three services:

  • Product Service — manages product catalog
  • Order Service — handles orders and payments
  • User Service — authentication and user management

Step 1: Define Service Contracts with Protobuf

Start with clear API contracts using Protocol Buffers:

// proto/product/product.proto
syntax = "proto3";

package product;
option go_package = "github.com/yourcompany/services/product";

service ProductService {
    rpc GetProduct(GetProductRequest) returns (Product);
    rpc ListProducts(ListProductsRequest) returns (ProductList);
    rpc CreateProduct(CreateProductRequest) returns (Product);
}

message Product {
    string id = 1;
    string name = 2;
    string description = 3;
    double price = 4;
    int32 stock = 5;
}

message GetProductRequest {
    string id = 1;
}

message ListProductsRequest {
    int32 page = 1;
    int32 limit = 2;
}

Step 2: Implement the Service

Generate Go code and implement the service:

# Generate Go code from protobuf
protoc --go_out=. --go-grpc_out=. proto/product/*.proto
// services/product/server.go
package main

import (
    "context"
    "log"
    "net"
    
    "google.golang.org/grpc"
    pb "github.com/yourcompany/services/product"
)

type server struct {
    pb.UnimplementedProductServiceServer
    db *sql.DB
}

func (s *server) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) {
    // Add tracing
    span, ctx := opentracing.StartSpanFromContext(ctx, "GetProduct")
    defer span.Finish()
    
    var product pb.Product
    err := s.db.QueryRowContext(ctx, 
        "SELECT id, name, description, price, stock FROM products WHERE id = $1",
        req.Id,
    ).Scan(&product.Id, &product.Name, &product.Description, &product.Price, &product.Stock)
    
    if err != nil {
        return nil, status.Errorf(codes.NotFound, "product not found: %v", err)
    }
    
    return &product, nil
}

func main() {
    // Setup database
    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    
    // Setup gRPC server
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatal(err)
    }
    
    // Add interceptors for logging, auth, metrics
    s := grpc.NewServer(
        grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
            grpc_prometheus.UnaryServerInterceptor,
            grpc_zap.UnaryServerInterceptor(logger),
            grpc_recovery.UnaryServerInterceptor(),
        )),
    )
    
    pb.RegisterProductServiceServer(s, &server{db: db})
    
    log.Printf("Server starting on port 50051")
    if err := s.Serve(lis); err != nil {
        log.Fatal(err)
    }
}

Step 3: Add Service Discovery

Use Consul or etcd for service discovery:

// pkg/discovery/consul.go
type ServiceDiscovery struct {
    client *consul.Client
}

func (sd *ServiceDiscovery) Register(name, address string, port int) error {
    return sd.client.Agent().ServiceRegister(&consul.AgentServiceRegistration{
        ID:      fmt.Sprintf("%s-%s", name, uuid.New()),
        Name:    name,
        Port:    port,
        Address: address,
        Check: &consul.AgentServiceCheck{
            GRPC:                           fmt.Sprintf("%s:%d", address, port),
            Interval:                       "10s",
            DeregisterCriticalServiceAfter: "30s",
        },
    })
}

func (sd *ServiceDiscovery) Discover(name string) ([]string, error) {
    services, _, err := sd.client.Health().Service(name, "", true, nil)
    if err != nil {
        return nil, err
    }
    
    var addresses []string
    for _, service := range services {
        addr := fmt.Sprintf("%s:%d", service.Service.Address, service.Service.Port)
        addresses = append(addresses, addr)
    }
    
    return addresses, nil
}

Step 4: Inter-Service Communication

Implement resilient service-to-service communication:

// pkg/client/grpc_client.go
type GRPCClient struct {
    conn    *grpc.ClientConn
    breaker *gobreaker.CircuitBreaker
}

func NewGRPCClient(serviceName string, discovery ServiceDiscovery) (*GRPCClient, error) {
    // Circuit breaker settings
    settings := gobreaker.Settings{
        Name:        serviceName,
        MaxRequests: 3,
        Interval:    time.Second * 10,
        Timeout:     time.Second * 30,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
            return counts.Requests >= 3 && failureRatio >= 0.6
        },
    }
    
    cb := gobreaker.NewCircuitBreaker(settings)
    
    // Load balancing with service discovery
    addresses, err := discovery.Discover(serviceName)
    if err != nil {
        return nil, err
    }
    
    // gRPC with retries and timeout
    conn, err := grpc.Dial(
        addresses[0], // In production, use proper load balancer
        grpc.WithInsecure(),
        grpc.WithUnaryInterceptor(grpc_retry.UnaryClientInterceptor(
            grpc_retry.WithMax(3),
            grpc_retry.WithBackoff(grpc_retry.BackoffExponential(100*time.Millisecond)),
        )),
        grpc.WithTimeout(5*time.Second),
    )
    
    return &GRPCClient{conn: conn, breaker: cb}, nil
}

Step 5: Containerize with Docker

Multi-stage build for minimal image size:

# Dockerfile
# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Final stage
FROM alpine:latest

RUN apk --no-cache add ca-certificates
WORKDIR /root/

COPY --from=builder /app/main .
COPY --from=builder /app/configs ./configs

EXPOSE 50051
CMD ["./main"]

Step 6: Deploy to Kubernetes

Kubernetes manifests for production deployment:

# k8s/product-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-service
  labels:
    app: product-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: product-service
  template:
    metadata:
      labels:
        app: product-service
    spec:
      containers:
      - name: product-service
        image: yourcompany/product-service:v1.0.0
        ports:
        - containerPort: 50051
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: url
        resources:
          requests:
            memory: "64Mi"
            cpu: "250m"
          limits:
            memory: "128Mi"
            cpu: "500m"
        livenessProbe:
          grpc:
            port: 50051
          initialDelaySeconds: 10
        readinessProbe:
          grpc:
            port: 50051
          initialDelaySeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: product-service
spec:
  selector:
    app: product-service
  ports:
    - protocol: TCP
      port: 50051
      targetPort: 50051
  type: ClusterIP

Step 7: Add Observability

Monitoring, logging, and tracing are crucial:

// pkg/observability/setup.go
func SetupObservability() {
    // Prometheus metrics
    prometheus.MustRegister(
        collectors.NewGoCollector(),
        collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
    )
    
    // Jaeger tracing
    cfg := config.Configuration{
        ServiceName: os.Getenv("SERVICE_NAME"),
        Sampler: &config.SamplerConfig{
            Type:  "const",
            Param: 1,
        },
        Reporter: &config.ReporterConfig{
            LogSpans:           true,
            LocalAgentHostPort: "jaeger:6831",
        },
    }
    
    tracer, closer, _ := cfg.NewTracer()
    opentracing.SetGlobalTracer(tracer)
    defer closer.Close()
    
    // Structured logging with Zap
    logger, _ := zap.NewProduction()
    defer logger.Sync()
    zap.ReplaceGlobals(logger)
}

Step 8: API Gateway

Expose services through a unified API gateway:

// gateway/main.go
func main() {
    r := gin.New()
    
    // Middleware
    r.Use(gin.Recovery())
    r.Use(cors.Default())
    r.Use(RateLimitMiddleware())
    r.Use(AuthMiddleware())
    
    // Product endpoints
    productClient := pb.NewProductServiceClient(getGRPCConn("product-service"))
    
    r.GET("/api/v1/products/:id", func(c *gin.Context) {
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()
        
        product, err := productClient.GetProduct(ctx, &pb.GetProductRequest{
            Id: c.Param("id"),
        })
        
        if err != nil {
            c.JSON(500, gin.H{"error": err.Error()})
            return
        }
        
        c.JSON(200, product)
    })
    
    r.Run(":8080")
}

Production Checklist

  • ✅ Health checks and readiness probes
  • ✅ Graceful shutdown
  • ✅ Circuit breakers for external calls
  • ✅ Distributed tracing
  • ✅ Structured logging
  • ✅ Metrics and monitoring
  • ✅ Rate limiting
  • ✅ Authentication & authorization
  • ✅ Database migrations
  • ✅ Configuration management
  • ✅ Secrets management
  • ✅ Horizontal pod autoscaling

Common Pitfalls to Avoid

  1. Not handling context properly — always propagate context for cancellation
  2. Ignoring idempotency — make operations retry-safe
  3. No circuit breakers — cascading failures will kill your system
  4. Synchronous everything — use message queues for long operations
  5. No distributed tracing — debugging becomes impossible

Conclusion

Building microservices in Go is straightforward when you follow best practices. Start simple, add complexity as needed, and always prioritize observability and resilience. The combination of Go's simplicity and the ecosystem's maturity makes it an excellent choice for microservices architecture.

Next Steps

  • Add event-driven communication with NATS or Kafka
  • Implement CQRS and Event Sourcing patterns
  • Set up GitOps with ArgoCD
  • Add service mesh with Istio
  • Implement distributed caching with Redis