Arquitetura de Microserviços com Go: Lições Aprendidas
Após anos trabalhando com microserviços em Go em empresas como Nubank e OLX, compartilho aqui as principais lições aprendidas sobre como estruturar, desenvolver e manter sistemas distribuídos robustos.
Por que Go para Microserviços?
Go se tornou uma escolha natural para microserviços devido a suas características únicas:
- Performance: Compilação para código nativo e garbage collector eficiente
- Concorrência: Goroutines e channels facilitam programação concorrente
- Simplicidade: Sintaxe limpa e curva de aprendizado suave
- Tooling: Excelente ecossistema de ferramentas
Padrões Arquiteturais Essenciais
1. Domain-Driven Design (DDD)
Estruturar o código seguindo os domínios do negócio:
// internal/domain/user/user.go type User struct { ID string Email string Name string Status UserStatus } type UserRepository interface { Save(ctx context.Context, user *User) error FindByID(ctx context.Context, id string) (*User, error) }
2. Hexagonal Architecture
Separar a lógica de negócio dos detalhes de implementação:
// internal/application/user_service.go type UserService struct { repo UserRepository eventBus EventBus } func (s *UserService) CreateUser(ctx context.Context, cmd CreateUserCommand) error { user := NewUser(cmd.Email, cmd.Name) if err := s.repo.Save(ctx, user); err != nil { return err } return s.eventBus.Publish(ctx, UserCreatedEvent{UserID: user.ID}) }
Comunicação Entre Serviços
HTTP vs gRPC vs Mensageria
HTTP REST: Ideal para APIs públicas e integrações simples gRPC: Melhor performance para comunicação interna Mensageria: Para eventos assíncronos e desacoplamento
Exemplo com gRPC
// proto/user/user.proto service UserService { rpc GetUser(GetUserRequest) returns (GetUserResponse); rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); } // internal/grpc/user_handler.go func (h *UserHandler) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) { user, err := h.userService.GetUser(ctx, req.UserId) if err != nil { return nil, status.Errorf(codes.NotFound, "user not found: %v", err) } return &pb.GetUserResponse{ User: &pb.User{ Id: user.ID, Email: user.Email, Name: user.Name, }, }, nil }
Observabilidade e Monitoramento
Structured Logging
import "go.uber.org/zap" logger, _ := zap.NewProduction() defer logger.Sync() logger.Info("user created", zap.String("user_id", user.ID), zap.String("email", user.Email), zap.Duration("duration", time.Since(start)), )
Métricas com Prometheus
var ( requestsTotal = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "http_requests_total", Help: "Total number of HTTP requests", }, []string{"method", "endpoint", "status"}, ) ) func init() { prometheus.MustRegister(requestsTotal) }
Testes em Microserviços
Pirâmide de Testes
- Unit Tests: Testam componentes isolados
- Integration Tests: Testam interação entre componentes
- Contract Tests: Garantem compatibilidade entre serviços
- End-to-End Tests: Testam fluxos completos
Exemplo de Test Container
func TestUserRepository(t *testing.T) { ctx := context.Background() // Inicia container PostgreSQL para teste postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ Image: "postgres:13", ExposedPorts: []string{"5432/tcp"}, Env: map[string]string{ "POSTGRES_PASSWORD": "password", "POSTGRES_DB": "testdb", }, }, Started: true, }) require.NoError(t, err) defer postgres.Terminate(ctx) // Testa repository com banco real repo := NewUserRepository(db) user := &User{ID: "123", Email: "test@example.com"} err = repo.Save(ctx, user) assert.NoError(t, err) }
Deployment e DevOps
Docker Multi-stage
## 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 -o main ./cmd/api ## Runtime stage FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /app/main . CMD ["./main"]
Kubernetes Deployment
apiVersion: apps/v1 kind: Deployment metadata: name: user-service spec: replicas: 3 selector: matchLabels: app: user-service template: metadata: labels: app: user-service spec: containers: - name: user-service image: user-service:latest ports: - containerPort: 8080 env: - name: DATABASE_URL valueFrom: secretKeyRef: name: db-secret key: url
Lições Aprendidas
1. Comece Simples
Não crie microserviços desde o início. Comece com um monólito bem estruturado e extraia serviços conforme necessário.
2. Observabilidade é Fundamental
Implemente logging, métricas e tracing desde o início. Sem observabilidade, debugar sistemas distribuídos é impossível.
3. Testes de Contrato
Use ferramentas como Pact para garantir que mudanças em um serviço não quebrem outros.
4. Circuit Breakers
Implemente circuit breakers para evitar cascata de falhas:
import "github.com/sony/gobreaker" cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{ Name: "external-api", MaxRequests: 3, Interval: time.Minute, Timeout: time.Minute * 2, }) result, err := cb.Execute(func() (interface{}, error) { return externalAPI.Call() })
5. Versionamento de APIs
Sempre versione suas APIs para permitir evolução sem quebrar clientes:
// v1/user.go type UserV1 struct { ID string `json:"id"` Name string `json:"name"` } // v2/user.go type UserV2 struct { ID string `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` }
Conclusão
Microserviços com Go oferecem uma base sólida para sistemas distribuídos, mas requerem disciplina arquitetural e operacional. As lições compartilhadas aqui foram aprendidas na prática, trabalhando em sistemas de alta escala.
O mais importante é lembrar que microserviços são uma ferramenta, não um objetivo. Use-os quando fizerem sentido para seu contexto específico.