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ística | REST/HTTP JSON | gRPC |
|---|---|---|
| Formato | JSON (texto) | Protocol Buffers (binário) |
| Performance | ~15x mais lento | ~15x mais rápido |
| Payload | Verbos, repetitivo | Compacto, eficiente |
| Tipagem | Fraca (runtime) | Forte (compile-time) |
| Streaming | Complicado (SSE/WebSocket) | Nativo e simples |
| Code Generation | Manual | Automático |
| Browser Support | Nativo | Requer 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 mensagensproto/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:
- Explore service meshes como Istio ou Linkerd para gerenciamento avançado
- Implemente tracing distribuído com OpenTelemetry
- Adicione métricas com Prometheus
- Estude gRPC-Gateway para expor APIs REST junto com gRPC
- 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.