Go e gRPC: Comunicação entre Serviços

O gRPC tornou-se o padrão de facto para comunicação entre microserviços em ambientes de alta performance. Desenvolvido pelo Google, ele oferece vantagens significativas sobre REST tradicional, especialmente quando combinado com a eficiência do Go.

Neste tutorial completo, você vai aprender a construir serviços gRPC robustos em Go, desde o básico até técnicas avançadas como streaming bidirecional e interceptores.

Por Que Usar gRPC em Go?

gRPC vs REST: Comparativo

CaracterísticaREST/HTTP JSONgRPC
FormatoJSON (texto)Protocol Buffers (binário)
Performance~15x mais lento~15x mais rápido
PayloadVerbos, repetitivoCompacto, eficiente
TipagemFraca (runtime)Forte (compile-time)
StreamingComplicado (SSE/WebSocket)Nativo e simples
Code GenerationManualAutomático
Browser SupportNativoRequer gRPC-Web

Quando escolher gRPC:

  • Comunicação entre microserviços internos
  • APIs de alta performance
  • Streaming de dados em tempo real
  • Ambientes com largura de banda limitada

Quando escolher REST:

  • APIs públicas para clientes externos
  • Integração com browsers (sem gRPC-Web)
  • APIs simples CRUD
  • Quando simplicidade é prioridade

Protocol Buffers: A Base do gRPC

O Protocol Buffers (protobuf) é o formato de serialização usado pelo gRPC. Ele define contratos de API de forma clara e gera código automaticamente.

Instalando as Ferramentas

# Instalar o compilador protobuf
# macOS
brew install protobuf

# Ubuntu/Debian
apt-get install -y protobuf-compiler

# Instalar plugins Go
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# Verificar instalação
protoc --version

Definindo Seu Primeiro Serviço

Crie o arquivo proto/calculator.proto:

syntax = "proto3";

option go_package = "github.com/seuusuario/calculadora/proto";

package calculator;

// Serviço de calculadora
service Calculator {
  // Operação simples
  rpc Add (OperationRequest) returns (OperationResponse);
  rpc Subtract (OperationRequest) returns (OperationResponse);
  rpc Multiply (OperationRequest) returns (OperationResponse);
  rpc Divide (OperationRequest) returns (OperationResponse);
  
  // Streaming
  rpc StreamOperations (stream OperationRequest) returns (stream OperationResponse);
}

// Mensagem de requisição
message OperationRequest {
  double num1 = 1;
  double num2 = 2;
  string operation_id = 3;
}

// Mensagem de resposta
message OperationResponse {
  double result = 1;
  bool success = 2;
  string error_message = 3;
}

Gerando Código Go

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       proto/calculator.proto

Isso gera dois arquivos:

  • proto/calculator.pb.go - Structs das mensagens
  • proto/calculator_grpc.pb.go - Interfaces do serviço e cliente

Criando um Servidor gRPC

Estrutura do Projeto

calculadora-grpc/
├── proto/
│   ├── calculator.proto
│   ├── calculator.pb.go
│   └── calculator_grpc.pb.go
├── server/
│   └── main.go
├── client/
│   └── main.go
└── go.mod

Implementando o Servidor

Crie server/main.go:

package main

import (
	"context"
	"fmt"
	"log"
	"net"

	pb "github.com/seuusuario/calculadora/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// server implementa a interface CalculatorServer
type server struct {
	pb.UnimplementedCalculatorServer
}

// Add implementa a operação de soma
func (s *server) Add(ctx context.Context, req *pb.OperationRequest) (*pb.OperationResponse, error) {
	result := req.Num1 + req.Num2
	
	log.Printf("Operação: %.2f + %.2f = %.2f", req.Num1, req.Num2, result)
	
	return &pb.OperationResponse{
		Result:  result,
		Success: true,
	}, nil
}

// Subtract implementa a operação de subtração
func (s *server) Subtract(ctx context.Context, req *pb.OperationRequest) (*pb.OperationResponse, error) {
	return &pb.OperationResponse{
		Result:  req.Num1 - req.Num2,
		Success: true,
	}, nil
}

// Multiply implementa a operação de multiplicação
func (s *server) Multiply(ctx context.Context, req *pb.OperationRequest) (*pb.OperationResponse, error) {
	return &pb.OperationResponse{
		Result:  req.Num1 * req.Num2,
		Success: true,
	}, nil
}

// Divide implementa a operação de divisão com tratamento de erro
func (s *server) Divide(ctx context.Context, req *pb.OperationRequest) (*pb.OperationResponse, error) {
	if req.Num2 == 0 {
		// Retornar erro gRPC apropriado
		return nil, status.Error(codes.InvalidArgument, "divisão por zero não permitida")
	}
	
	return &pb.OperationResponse{
		Result:  req.Num1 / req.Num2,
		Success: true,
	}, nil
}

func main() {
	// Criar listener TCP
	listener, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("Falha ao criar listener: %v", err)
	}

	// Criar servidor gRPC
	grpcServer := grpc.NewServer()
	
	// Registrar serviço
	pb.RegisterCalculatorServer(grpcServer, &server{})

	fmt.Println("Servidor gRPC iniciado na porta :50051")
	
	// Iniciar servidor
	if err := grpcServer.Serve(listener); err != nil {
		log.Fatalf("Falha ao iniciar servidor: %v", err)
	}
}

Inicializando e Executando

# Inicializar módulo
go mod init github.com/seuusuario/calculadora

# Baixar dependências
go mod tidy

# Executar servidor
go run server/main.go
# Saída: Servidor gRPC iniciado na porta :50051

Criando um Cliente gRPC

Cliente Básico

Crie client/main.go:

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	pb "github.com/seuusuario/calculadora/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	// Configurar conexão com timeout
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// Conectar ao servidor
	conn, err := grpc.DialContext(ctx, "localhost:50051",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithBlock(),
	)
	if err != nil {
		log.Fatalf("Falha ao conectar: %v", err)
	}
	defer conn.Close()

	// Criar cliente
	client := pb.NewCalculatorClient(conn)

	// Criar contexto para requisições
	requestCtx, requestCancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer requestCancel()

	// Testar operações
	operations := []struct {
		name string
		num1 float64
		num2 float64
		op   func(context.Context, *pb.OperationRequest) (*pb.OperationResponse, error)
	}{
		{"Soma", 10, 5, client.Add},
		{"Subtração", 10, 5, client.Subtract},
		{"Multiplicação", 10, 5, client.Multiply},
		{"Divisão", 10, 5, client.Divide},
	}

	for _, op := range operations {
		req := &pb.OperationRequest{
			Num1:         op.num1,
			Num2:         op.num2,
			OperationId:  fmt.Sprintf("op-%s", op.name),
		}

		resp, err := op.op(requestCtx, req)
		if err != nil {
			log.Printf("Erro em %s: %v", op.name, err)
			continue
		}

		fmt.Printf("%s: %.2f e %.2f = %.2f (sucesso: %v)\n",
			op.name, op.num1, op.num2, resp.Result, resp.Success)
	}

	// Testar divisão por zero
	_, err = client.Divide(requestCtx, &pb.OperationRequest{
		Num1: 10,
		Num2: 0,
	})
	if err != nil {
		fmt.Printf("Erro esperado na divisão por zero: %v\n", err)
	}
}

Executando o Cliente

# Em outro terminal
go run client/main.go

Streaming em gRPC

Um dos diferenciais do gRPC é o suporte nativo a streaming. Vamos implementar streaming bidirecional:

Adicionando Streaming ao Proto

service Calculator {
  // ... métodos anteriores ...
  
  // Streaming de estatísticas
  rpc StreamStats (stream Number) returns (stream StatsResponse);
}

message Number {
  double value = 1;
}

message StatsResponse {
  double count = 1;
  double sum = 2;
  double average = 3;
  double min = 4;
  double max = 5;
}

Implementando Streaming no Servidor

func (s *server) StreamStats(stream pb.Calculator_StreamStatsServer) error {
	var count int64
	var sum, min, max float64
	first := true

	for {
		// Receber número do cliente
		num, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}

		// Atualizar estatísticas
		count++
		sum += num.Value

		if first {
			min = num.Value
			max = num.Value
			first = false
		} else {
			if num.Value < min {
				min = num.Value
			}
			if num.Value > max {
				max = num.Value
			}
		}

		average := sum / float64(count)

		// Enviar estatísticas atualizadas
		resp := &pb.StatsResponse{
			Count:   float64(count),
			Sum:     sum,
			Average: average,
			Min:     min,
			Max:     max,
		}

		if err := stream.Send(resp); err != nil {
			return err
		}
	}
}

Cliente com Streaming

func testStreaming(client pb.CalculatorClient) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	stream, err := client.StreamStats(ctx)
	if err != nil {
		log.Fatalf("Erro ao iniciar stream: %v", err)
	}

	// Goroutine para receber respostas
	go func() {
		for {
			resp, err := stream.Recv()
			if err == io.EOF {
				return
			}
			if err != nil {
				log.Printf("Erro ao receber: %v", err)
				return
			}
			
			fmt.Printf("Stats: count=%.0f, sum=%.2f, avg=%.2f, min=%.2f, max=%.2f\n",
				resp.Count, resp.Sum, resp.Average, resp.Min, resp.Max)
		}
	}()

	// Enviar números
	numbers := []float64{10, 20, 30, 40, 50}
	for _, n := range numbers {
		if err := stream.Send(&pb.Number{Value: n}); err != nil {
			log.Printf("Erro ao enviar: %v", err)
			return
		}
		time.Sleep(500 * time.Millisecond)
	}

	stream.CloseSend()
	time.Sleep(time.Second)
}

Interceptores (Middleware)

Interceptores permitem adicionar comportamentos cross-cutting como logging, autenticação e métricas.

Interceptor de Logging

func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	start := time.Now()
	
	log.Printf("[gRPC] Método: %s | Requisição: %+v", info.FullMethod, req)
	
	resp, err := handler(ctx, req)
	
	duration := time.Since(start)
	
	if err != nil {
		log.Printf("[gRPC] Método: %s | Erro: %v | Duração: %v", info.FullMethod, err, duration)
	} else {
		log.Printf("[gRPC] Método: %s | Resposta: %+v | Duração: %v", info.FullMethod, resp, duration)
	}
	
	return resp, err
}

// Registrar no servidor
grpcServer := grpc.NewServer(
	grpc.UnaryInterceptor(loggingInterceptor),
)

Interceptor de Autenticação

func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	// Extrair metadados
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, status.Error(codes.Unauthenticated, "metadados não encontrados")
	}

	// Verificar token
	tokens := md.Get("authorization")
	if len(tokens) == 0 {
		return nil, status.Error(codes.Unauthenticated, "token não fornecido")
	}

	token := strings.TrimPrefix(tokens[0], "Bearer ")
	if !isValidToken(token) {
		return nil, status.Error(codes.Unauthenticated, "token inválido")
	}

	// Adicionar claims ao contexto
	ctx = context.WithValue(ctx, "user_id", getUserIDFromToken(token))
	
	return handler(ctx, req)
}

func isValidToken(token string) bool {
	// Implementar validação JWT
	return token == "seu-token-secreto"
}

Cliente com Autenticação

func authInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	// Adicionar token às requisições
	ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer seu-token-aqui")
	return invoker(ctx, method, req, reply, cc, opts...)
}

// Usar no cliente
conn, err := grpc.Dial("localhost:50051",
	grpc.WithTransportCredentials(insecure.NewCredentials()),
	grpc.WithUnaryInterceptor(authInterceptor),
)

Testando Serviços gRPC

Testes Unitários

package main

import (
	"context"
	"testing"

	pb "github.com/seuusuario/calculadora/proto"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type mockServer struct {
	pb.UnimplementedCalculatorServer
}

func TestAdd(t *testing.T) {
	s := &server{}
	ctx := context.Background()

	tests := []struct {
		name     string
		req      *pb.OperationRequest
		expected float64
		wantErr  bool
	}{
		{
			name:     "soma positiva",
			req:      &pb.OperationRequest{Num1: 10, Num2: 5},
			expected: 15,
			wantErr:  false,
		},
		{
			name:     "soma com negativos",
			req:      &pb.OperationRequest{Num1: -10, Num2: 5},
			expected: -5,
			wantErr:  false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			resp, err := s.Add(ctx, tt.req)
			
			if (err != nil) != tt.wantErr {
				t.Errorf("Add() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			
			if !tt.wantErr && resp.Result != tt.expected {
				t.Errorf("Add() = %v, want %v", resp.Result, tt.expected)
			}
		})
	}
}

func TestDivideByZero(t *testing.T) {
	s := &server{}
	ctx := context.Background()

	req := &pb.OperationRequest{Num1: 10, Num2: 0}
	_, err := s.Divide(ctx, req)

	if err == nil {
		t.Fatal("esperava erro na divisão por zero")
	}

	st, ok := status.FromError(err)
	if !ok {
		t.Fatal("esperava status gRPC")
	}

	if st.Code() != codes.InvalidArgument {
		t.Errorf("código esperado %v, obtido %v", codes.InvalidArgument, st.Code())
	}
}

Testes de Integração

func TestIntegration(t *testing.T) {
	// Iniciar servidor em porta aleatória
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("falha ao criar listener: %v", err)
	}

	grpcServer := grpc.NewServer()
	pb.RegisterCalculatorServer(grpcServer, &server{})

	go grpcServer.Serve(listener)
	defer grpcServer.Stop()

	// Conectar cliente
	conn, err := grpc.Dial(listener.Addr().String(),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		t.Fatalf("falha ao conectar: %v", err)
	}
	defer conn.Close()

	client := pb.NewCalculatorClient(conn)
	ctx := context.Background()

	// Testar operações
	resp, err := client.Add(ctx, &pb.OperationRequest{Num1: 10, Num2: 20})
	if err != nil {
		t.Fatalf("Add falhou: %v", err)
	}

	if resp.Result != 30 {
		t.Errorf("resultado incorreto: got %v, want 30", resp.Result)
	}
}

Melhores Práticas

1. Tratamento de Erros

Use códigos de status gRPC apropriados:

import "google.golang.org/grpc/codes"
import "google.golang.org/grpc/status"

// Códigos comuns:
// codes.OK           - Sucesso
// codes.NotFound     - Recurso não encontrado
// codes.InvalidArgument - Argumento inválido
// codes.Internal     - Erro interno do servidor
// codes.Unavailable  - Serviço indisponível
// codes.DeadlineExceeded - Timeout

func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    user, err := s.db.GetUser(req.Id)
    if err == sql.ErrNoRows {
        return nil, status.Errorf(codes.NotFound, "usuário %d não encontrado", req.Id)
    }
    if err != nil {
        return nil, status.Errorf(codes.Internal, "erro ao buscar usuário: %v", err)
    }
    return user, nil
}

2. Timeouts e Contextos

// Sempre use contextos com timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

resp, err := client.Operation(ctx, req)
if err != nil {
    if status.Code(err) == codes.DeadlineExceeded {
        // Tratar timeout especificamente
        log.Println("operação excedeu o tempo limite")
    }
}

3. Health Checks

import "google.golang.org/grpc/health"
import "google.golang.org/grpc/health/grpc_health_v1"

// Registrar health check
healthServer := health.NewServer()
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)

4. Load Balancing

// Client-side load balancing
conn, err := grpc.Dial(
	"dns:///servico.exemplo.com",
	grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
	grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
)

Deploy e Monitoramento

Docker

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 server ./server

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/server .
EXPOSE 50051
CMD ["./server"]

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: calculadora-grpc
spec:
  replicas: 3
  selector:
    matchLabels:
      app: calculadora
  template:
    metadata:
      labels:
        app: calculadora
    spec:
      containers:
      - name: server
        image: calculadora-grpc:latest
        ports:
        - containerPort: 50051
        livenessProbe:
          grpc:
            port: 50051
          initialDelaySeconds: 10
        readinessProbe:
          grpc:
            port: 50051
          initialDelaySeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: calculadora-service
spec:
  selector:
    app: calculadora
  ports:
  - port: 50051
    targetPort: 50051
  type: ClusterIP

Próximos Passos

Agora que você domina o básico do gRPC com Go:

  1. Explore service meshes como Istio ou Linkerd para gerenciamento avançado
  2. Implemente tracing distribuído com OpenTelemetry
  3. Adicione métricas com Prometheus
  4. Estude gRPC-Gateway para expor APIs REST junto com gRPC
  5. Aprenda sobre Protocol Buffers avançado como oneof, maps e custom options

Conclusão

O gRPC oferece uma alternativa robusta e eficiente ao REST para comunicação entre serviços. Combinado com Go, você tem uma stack de alta performance para microserviços.

Pontos chave para lembrar:

  • Protobuf garante contratos de API type-safe
  • Streaming é nativo e eficiente
  • Interceptores permitem comportamentos cross-cutting
  • Testes são simples com as ferramentas certas
  • Códigos de status gRPC padronizam tratamento de erros

FAQ

Q: Posso usar gRPC com frontend web? R: Sim, através do gRPC-Web, que requer um proxy como Envoy entre o browser e o servidor gRPC.

Q: Como faço para versionar APIs gRPC? R: Use package names com versão (ex: v1, v2) ou mantenha compatibilidade backward com campos opcionais.

Q: gRPC é compatível com GraphQL? R: São tecnologias diferentes, mas você pode usar gRPC-Gateway para gerar REST e então GraphQL via ferramentas como gqlgen.

Q: Qual a diferença entre unary, server streaming, client streaming e bidirectional streaming? R: Unary = 1 req, 1 resp. Server streaming = 1 req, N resp. Client streaming = N req, 1 resp. Bidirectional = N req, N resp simultaneamente.


Continue sua jornada em Go explorando nosso tutorial sobre Go e Kubernetes para deploy de microserviços.