Artigo 51 — gRPC e Protocol Buffers: comunicação eficiente entre serviços
Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 9 — Deploy, Cloud e Carreira
Além do REST: quando a eficiência importa
REST com JSON funciona muito bem para a maioria das APIs. Mas em arquiteturas de microsserviços onde centenas de serviços se comunicam entre si com milhares de requisições por segundo, o custo de serializar e deserializar JSON, negociar headers HTTP/1.1 e reabrir conexões a cada requisição se torna mensurável.
gRPC resolve esses problemas com três escolhas fundamentais: Protocol Buffers como formato de serialização binária (compacto e rápido), HTTP/2 como transporte (multiplexação, compressão de headers, streaming bidirecional) e geração automática de código cliente e servidor a partir de definições de interface. O resultado é comunicação tipada, eficiente e com contrato formal.
Instalação das ferramentas
# Compilador de Protocol Buffers
# Linux
apt install protobuf-compiler
# macOS
brew install protobuf
# Plugins Go para protoc
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# Adicionar ao PATH
export PATH="$PATH:$(go env GOPATH)/bin"
# Dependências Go
go get google.golang.org/grpc
go get google.golang.org/protobuf
Protocol Buffers: definindo contratos
O arquivo .proto é o coração do gRPC — define mensagens, serviços e métodos de forma agnóstica à linguagem:
// proto/tarefa/v1/tarefa.proto
syntax = "proto3";
package tarefa.v1;
option go_package = "github.com/usuario/taskapi/gen/tarefa/v1;tarefav1";
import "google/protobuf/timestamp.proto";
// Enumeração de prioridade
enum Prioridade {
PRIORIDADE_DESCONHECIDA = 0; // valor zero obrigatório
PRIORIDADE_BAIXA = 1;
PRIORIDADE_MEDIA = 2;
PRIORIDADE_ALTA = 3;
PRIORIDADE_URGENTE = 4;
}
// Mensagem de tarefa
message Tarefa {
int64 id = 1;
int64 usuario_id = 2;
string titulo = 3;
string descricao = 4;
bool concluida = 5;
Prioridade prioridade = 6;
google.protobuf.Timestamp prazo = 7;
google.protobuf.Timestamp criado_em = 8;
google.protobuf.Timestamp atualizado_em = 9;
}
// Requisições e respostas
message CriarTarefaRequest {
int64 usuario_id = 1;
string titulo = 2;
string descricao = 3;
Prioridade prioridade = 4;
google.protobuf.Timestamp prazo = 5;
}
message CriarTarefaResponse {
Tarefa tarefa = 1;
}
message BuscarTarefaRequest {
int64 id = 1;
int64 usuario_id = 2;
}
message BuscarTarefaResponse {
Tarefa tarefa = 1;
}
message ListarTarefasRequest {
int64 usuario_id = 1;
int32 pagina = 2;
int32 limite = 3;
optional bool concluida = 4;
optional Prioridade prioridade = 5;
}
message ListarTarefasResponse {
repeated Tarefa tarefas = 1;
int64 total = 2;
}
message AtualizarTarefaRequest {
int64 id = 1;
int64 usuario_id = 2;
string titulo = 3;
string descricao = 4;
bool concluida = 5;
Prioridade prioridade = 6;
google.protobuf.Timestamp prazo = 7;
}
message AtualizarTarefaResponse {
Tarefa tarefa = 1;
}
message DeletarTarefaRequest {
int64 id = 1;
int64 usuario_id = 2;
}
message DeletarTarefaResponse {}
// Definição do serviço
service TarefaService {
rpc CriarTarefa (CriarTarefaRequest) returns (CriarTarefaResponse);
rpc BuscarTarefa (BuscarTarefaRequest) returns (BuscarTarefaResponse);
rpc ListarTarefas (ListarTarefasRequest) returns (ListarTarefasResponse);
rpc AtualizarTarefa(AtualizarTarefaRequest) returns (AtualizarTarefaResponse);
rpc DeletarTarefa (DeletarTarefaRequest) returns (DeletarTarefaResponse);
// Streaming server-side: recebe eventos em tempo real
rpc AssistirTarefas(AssistirTarefasRequest) returns (stream TarefaEvento);
}
message AssistirTarefasRequest {
int64 usuario_id = 1;
}
message TarefaEvento {
string tipo = 1; // "criada", "atualizada", "deletada"
Tarefa tarefa = 2;
google.protobuf.Timestamp ocorrido_em = 3;
}
Gerando código Go
# Estrutura do projeto
mkdir -p proto/tarefa/v1 gen/tarefa/v1
# Gerar código Go
protoc \
--proto_path=proto \
--go_out=gen \
--go_opt=paths=source_relative \
--go-grpc_out=gen \
--go-grpc_opt=paths=source_relative \
proto/tarefa/v1/tarefa.proto
O protoc gera dois arquivos:
- gen/tarefa/v1/tarefa.pb.go — structs e serialização
- gen/tarefa/v1/tarefa_grpc.pb.go — interfaces de servidor e cliente
# Makefile
.PHONY: proto
proto:
find proto -name "*.proto" | xargs -I{} protoc \
--proto_path=proto \
--go_out=gen \
--go_opt=paths=source_relative \
--go-grpc_out=gen \
--go-grpc_opt=paths=source_relative \
{}
Implementando o servidor gRPC
// internal/grpc/tarefa_server.go
package grpcserver
import (
"context"
"errors"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
tarefav1 "github.com/usuario/taskapi/gen/tarefa/v1"
"github.com/usuario/taskapi/internal/domain"
"github.com/usuario/taskapi/internal/repository"
"github.com/usuario/taskapi/internal/usecase/tarefa"
)
type TarefaServer struct {
tarefav1.UnimplementedTarefaServiceServer // embedding obrigatório para compatibilidade futura
criar *tarefa.CriarTarefa
buscar *tarefa.BuscarTarefa
listar *tarefa.ListarTarefas
atualizar *tarefa.AtualizarTarefa
deletar *tarefa.DeletarTarefa
eventos chan TarefaEventoInterno
}
type TarefaEventoInterno struct {
Tipo string
Tarefa *domain.Tarefa
}
func NovoTarefaServer(
criar *tarefa.CriarTarefa,
buscar *tarefa.BuscarTarefa,
listar *tarefa.ListarTarefas,
atualizar *tarefa.AtualizarTarefa,
deletar *tarefa.DeletarTarefa,
) *TarefaServer {
return &TarefaServer{
criar: criar,
buscar: buscar,
listar: listar,
atualizar: atualizar,
deletar: deletar,
eventos: make(chan TarefaEventoInterno, 100),
}
}
func (s *TarefaServer) CriarTarefa(
ctx context.Context,
req *tarefav1.CriarTarefaRequest,
) (*tarefav1.CriarTarefaResponse, error) {
var prazo *time.Time
if req.Prazo != nil {
t := req.Prazo.AsTime()
prazo = &t
}
t, err := s.criar.Executar(ctx, tarefa.CriarInput{
UsuarioID: req.UsuarioId,
Titulo: req.Titulo,
Descricao: req.Descricao,
Prioridade: domain.PrioridadeTarefa(req.Prioridade),
Prazo: prazo,
})
if err != nil {
return nil, traduzirErroGRPC(err)
}
// Publica evento para streaming
go func() {
s.eventos <- TarefaEventoInterno{Tipo: "criada", Tarefa: t}
}()
return &tarefav1.CriarTarefaResponse{
Tarefa: domainParaProto(t),
}, nil
}
func (s *TarefaServer) BuscarTarefa(
ctx context.Context,
req *tarefav1.BuscarTarefaRequest,
) (*tarefav1.BuscarTarefaResponse, error) {
t, err := s.buscar.Executar(ctx, req.Id, req.UsuarioId)
if err != nil {
return nil, traduzirErroGRPC(err)
}
return &tarefav1.BuscarTarefaResponse{
Tarefa: domainParaProto(t),
}, nil
}
func (s *TarefaServer) ListarTarefas(
ctx context.Context,
req *tarefav1.ListarTarefasRequest,
) (*tarefav1.ListarTarefasResponse, error) {
filtro := repository.FiltroTarefa{
Pagina: int(req.Pagina),
Limite: int(req.Limite),
}
if req.Concluida != nil {
filtro.Concluida = req.Concluida
}
resultado, err := s.listar.Executar(ctx, tarefa.ListarInput{
UsuarioID: req.UsuarioId,
Filtro: filtro,
})
if err != nil {
return nil, traduzirErroGRPC(err)
}
protoTarefas := make([]*tarefav1.Tarefa, len(resultado.Tarefas))
for i, t := range resultado.Tarefas {
protoTarefas[i] = domainParaProto(t)
}
return &tarefav1.ListarTarefasResponse{
Tarefas: protoTarefas,
Total: resultado.Total,
}, nil
}
// AssistirTarefas implementa streaming server-side
func (s *TarefaServer) AssistirTarefas(
req *tarefav1.AssistirTarefasRequest,
stream tarefav1.TarefaService_AssistirTarefasServer,
) error {
ctx := stream.Context()
for {
select {
case <-ctx.Done():
return ctx.Err()
case evento := <-s.eventos:
// Filtra apenas eventos do usuário que está assistindo
if evento.Tarefa.UsuarioID != req.UsuarioId {
continue
}
if err := stream.Send(&tarefav1.TarefaEvento{
Tipo: evento.Tipo,
Tarefa: domainParaProto(evento.Tarefa),
OcorridoEm: timestamppb.Now(),
}); err != nil {
return err
}
}
}
}
// Converte domínio para proto
func domainParaProto(t *domain.Tarefa) *tarefav1.Tarefa {
proto := &tarefav1.Tarefa{
Id: t.ID,
UsuarioId: t.UsuarioID,
Titulo: t.Titulo,
Descricao: t.Descricao,
Concluida: t.Concluida,
Prioridade: tarefav1.Prioridade(t.Prioridade),
CriadoEm: timestamppb.New(t.CriadoEm),
AtualizadoEm: timestamppb.New(t.AtualizadoEm),
}
if t.Prazo != nil {
proto.Prazo = timestamppb.New(*t.Prazo)
}
return proto
}
// Traduz erros de domínio para códigos gRPC
func traduzirErroGRPC(err error) error {
switch {
case errors.Is(err, domain.ErrTarefaNaoEncontrada):
return status.Error(codes.NotFound, err.Error())
case errors.Is(err, domain.ErrTituloDeTarefaVazio):
return status.Error(codes.InvalidArgument, err.Error())
case errors.Is(err, domain.ErrTarefaSemPermissao):
return status.Error(codes.PermissionDenied, err.Error())
case errors.Is(err, context.DeadlineExceeded):
return status.Error(codes.DeadlineExceeded, err.Error())
case errors.Is(err, context.Canceled):
return status.Error(codes.Canceled, err.Error())
default:
return status.Error(codes.Internal, "erro interno")
}
}
Interceptors: middlewares do gRPC
// internal/grpc/interceptors.go
package grpcserver
import (
"context"
"log/slog"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// Interceptor de logging unário
func InterceptorLogging(logger *slog.Logger) grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
inicio := time.Now()
resp, err := handler(ctx, req)
code := codes.OK
if err != nil {
code = status.Code(err)
}
logger.Info("gRPC unário",
"método", info.FullMethod,
"código", code,
"duração_ms", time.Since(inicio).Milliseconds(),
"erro", err,
)
return resp, err
}
}
// Interceptor de recuperação de pânico
func InterceptorRecuperacao() grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
defer func() {
if r := recover(); r != nil {
slog.Error("pânico no gRPC",
"método", info.FullMethod,
"panic", r,
)
err = status.Errorf(codes.Internal, "erro interno")
}
}()
return handler(ctx, req)
}
}
// Interceptor de autenticação JWT
func InterceptorAuth(segredo string) grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// Métodos públicos não precisam de autenticação
metodosSemAuth := map[string]bool{
"/tarefa.v1.TarefaService/HealthCheck": true,
}
if metodosSemAuth[info.FullMethod] {
return handler(ctx, req)
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "metadata ausente")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "token ausente")
}
claims, err := validarJWT(tokens[0], segredo)
if err != nil {
return nil, status.Error(codes.Unauthenticated, "token inválido")
}
ctx = context.WithValue(ctx, chaveUsuarioID{}, claims.UserID)
return handler(ctx, req)
}
}
Servidor principal
// cmd/grpc/main.go
package main
import (
"fmt"
"log/slog"
"net"
"os"
"os/signal"
"syscall"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
tarefav1 "github.com/usuario/taskapi/gen/tarefa/v1"
grpcserver "github.com/usuario/taskapi/internal/grpc"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// ... inicialização de banco, repositórios e casos de uso
// Servidor gRPC com interceptors encadeados
srv := grpc.NewServer(
grpc.ChainUnaryInterceptor(
grpcserver.InterceptorRecuperacao(),
grpcserver.InterceptorLogging(logger),
grpcserver.InterceptorAuth(os.Getenv("JWT_SECRET")),
),
grpc.ChainStreamInterceptor(
grpcserver.InterceptorStreamLogging(logger),
),
grpc.MaxRecvMsgSize(4*1024*1024), // 4 MB
grpc.MaxSendMsgSize(4*1024*1024),
)
// Registra o serviço
tarefaServer := grpcserver.NovoTarefaServer(
criarTarefa, buscarTarefa, listarTarefas, atualizarTarefa, deletarTarefa,
)
tarefav1.RegisterTarefaServiceServer(srv, tarefaServer)
// Reflection permite ferramentas como grpcurl descobrir serviços
reflection.Register(srv)
// Inicia o listener
porta := os.Getenv("GRPC_PORT")
if porta == "" {
porta = "50051"
}
lis, err := net.Listen("tcp", ":"+porta)
if err != nil {
logger.Error("falha ao escutar", "err", err)
os.Exit(1)
}
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
logger.Info("servidor gRPC iniciado", "porta", porta)
if err := srv.Serve(lis); err != nil {
logger.Error("servidor encerrado", "err", err)
}
}()
<-quit
logger.Info("encerrando servidor gRPC...")
srv.GracefulStop()
logger.Info("servidor encerrado")
}
Cliente gRPC
// internal/grpc/cliente/tarefa_cliente.go
package cliente
import (
"context"
"fmt"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
tarefav1 "github.com/usuario/taskapi/gen/tarefa/v1"
)
type ClienteTarefa struct {
conn *grpc.ClientConn
client tarefav1.TarefaServiceClient
}
func NovoClienteTarefa(endereco string) (*ClienteTarefa, error) {
conn, err := grpc.NewClient(
endereco,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(4*1024*1024),
),
)
if err != nil {
return nil, fmt.Errorf("conectar ao servidor gRPC: %w", err)
}
return &ClienteTarefa{
conn: conn,
client: tarefav1.NewTarefaServiceClient(conn),
}, nil
}
func (c *ClienteTarefa) Fechar() error {
return c.conn.Close()
}
func (c *ClienteTarefa) comToken(ctx context.Context, token string) context.Context {
md := metadata.New(map[string]string{
"authorization": "Bearer " + token,
})
return metadata.NewOutgoingContext(ctx, md)
}
func (c *ClienteTarefa) CriarTarefa(
ctx context.Context,
token string,
usuarioID int64,
titulo, descricao string,
prioridade tarefav1.Prioridade,
) (*tarefav1.Tarefa, error) {
ctx = c.comToken(ctx, token)
ctx, cancelar := context.WithTimeout(ctx, 5*time.Second)
defer cancelar()
resp, err := c.client.CriarTarefa(ctx, &tarefav1.CriarTarefaRequest{
UsuarioId: usuarioID,
Titulo: titulo,
Descricao: descricao,
Prioridade: prioridade,
})
if err != nil {
return nil, fmt.Errorf("criar tarefa: %w", err)
}
return resp.Tarefa, nil
}
// AssistirTarefas recebe eventos em tempo real via streaming
func (c *ClienteTarefa) AssistirTarefas(
ctx context.Context,
token string,
usuarioID int64,
onEvento func(*tarefav1.TarefaEvento),
) error {
ctx = c.comToken(ctx, token)
stream, err := c.client.AssistirTarefas(ctx, &tarefav1.AssistirTarefasRequest{
UsuarioId: usuarioID,
})
if err != nil {
return fmt.Errorf("iniciar streaming: %w", err)
}
for {
evento, err := stream.Recv()
if err != nil {
return fmt.Errorf("receber evento: %w", err)
}
onEvento(evento)
}
}
Testando com grpcurl
# Instalar grpcurl
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
# Listar serviços disponíveis (requer reflection)
grpcurl -plaintext localhost:50051 list
# Descrever serviço
grpcurl -plaintext localhost:50051 describe tarefa.v1.TarefaService
# Criar tarefa
grpcurl -plaintext \
-H "authorization: Bearer SEU_TOKEN" \
-d '{"usuario_id": 1, "titulo": "Estudar gRPC", "prioridade": "PRIORIDADE_ALTA"}' \
localhost:50051 \
tarefa.v1.TarefaService/CriarTarefa
# Listar tarefas
grpcurl -plaintext \
-H "authorization: Bearer SEU_TOKEN" \
-d '{"usuario_id": 1, "pagina": 1, "limite": 10}' \
localhost:50051 \
tarefa.v1.TarefaService/ListarTarefas
# Streaming
grpcurl -plaintext \
-H "authorization: Bearer SEU_TOKEN" \
-d '{"usuario_id": 1}' \
localhost:50051 \
tarefa.v1.TarefaService/AssistirTarefas
gRPC-Gateway: REST e gRPC da mesma definição
O grpc-gateway gera automaticamente um proxy HTTP/REST a partir das definições proto:
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
// Adicione ao proto com opções HTTP
import "google/api/annotations.proto";
service TarefaService {
rpc CriarTarefa (CriarTarefaRequest) returns (CriarTarefaResponse) {
option (google.api.http) = {
post: "/api/v1/tarefas"
body: "*"
};
}
rpc ListarTarefas (ListarTarefasRequest) returns (ListarTarefasResponse) {
option (google.api.http) = {
get: "/api/v1/tarefas"
};
}
rpc BuscarTarefa (BuscarTarefaRequest) returns (BuscarTarefaResponse) {
option (google.api.http) = {
get: "/api/v1/tarefas/{id}"
};
}
}
Com isso, a mesma implementação serve tanto clientes gRPC quanto clientes REST — sem duplicar código.
Resumo do que foi coberto
Este artigo apresentou gRPC e Protocol Buffers em Go de forma completa: definição de mensagens e serviços em .proto, geração de código com protoc, implementação do servidor com os quatro padrões de RPC — unário, streaming server, streaming client e streaming bidirecional — interceptors para logging, autenticação e recuperação de pânico, cliente gRPC com metadata para tokens JWT, streaming em tempo real, testes com grpcurl e grpc-gateway para expor REST automaticamente. O próximo e último artigo explora o ecossistema Go em 2025.
Referências e leituras complementares
-
Documentação oficial gRPC Go — Guia completo com exemplos. https://grpc.io/docs/languages/go/
-
Protocol Buffers — Go — Referência da linguagem proto3. https://protobuf.dev/getting-started/gotutorial/
-
grpc-gateway — Proxy REST automático a partir de definições proto. https://github.com/grpc-ecosystem/grpc-gateway
-
grpcurl — Cliente gRPC de linha de comando. https://github.com/fullstorydev/grpcurl
-
buf — Ferramenta moderna para gerenciar arquivos proto. https://buf.build
-
Evans — Cliente gRPC interativo para desenvolvimento. https://github.com/ktr0731/evans
Próximo artigo: Artigo 52 — O ecossistema Go em 2025: comunidade, carreira e próximos passos**
you asked
Sim