Golang

Artigo 51 — gRPC e Protocol Buffers: comunicação eficiente entre serviços Já leu

12 min de leitura

Artigo 51 — gRPC e Protocol Buffers: comunicação eficiente entre serviços
Artigo 51 — gRPC e Protocol Buffers: comunicação eficiente entre serviços Além do REST: quando a eficiência importa REST com JSON funciona muito bem para a

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


claude response

Comentários

Mais em Golang

Artigo 32 — Introdução ao database/sql e drivers em Go
Artigo 32 — Introdução ao database/sql e drivers em Go

Artigo 32 — Introdução ao database/sql e drivers em Go A camada de abstração...

Artigo 44 — Clean Architecture em Go: camadas e dependências
Artigo 44 — Clean Architecture em Go: camadas e dependências

Artigo 44 — Clean Architecture em Go: camadas e dependências Por que arquitet...

Go com PostgreSQL: recursos avançados e o driver pgx
Go com PostgreSQL: recursos avançados e o driver pgx

PostgreSQL é frequentemente descrito como o banco de dados relacional mais co...