Marco OllivierVoltar ao Blog
Arquitetura

Arquitetura de Microserviços com Go: Lições Aprendidas

15 de janeiro de 2024
4 min de leitura
Marco Ollivier
GoMicroserviçosArquiteturaBackend

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

  1. Unit Tests: Testam componentes isolados
  2. Integration Tests: Testam interação entre componentes
  3. Contract Tests: Garantem compatibilidade entre serviços
  4. 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.

© 2026 Marco Ollivier. Sr Staff Engineer com foco em comunidade e inovação.

me@marcopollivier.dev