Pular para o conteúdo

gRPC em Go

Publicada em · 24 min de leitura

Tamanho da fonte:
Largura da coluna:

Imagine que você precisa pedir uma informação para um colega que trabalha em outro departamento da sua empresa. No modelo clássico da web com REST e JSON, o processo se parece com escrever uma carta detalhada em uma folha de papel, colocar em um envelope com vários carimbos institucionais repetidos, despachar pelo correio interno e esperar o envelope voltar dias depois para abrir e ler cada linha do texto. O gRPC funciona como instalar um ramal telefônico privativo de discagem rápida na sua mesa: você aperta três dígitos, chama a função do colega diretamente e ele atende no mesmo instante, respondendo com um código binário compacto que você já sabe exatamente como interpretar.

No desenvolvimento de sistemas distribuídos modernos em Go, o gRPC (Google ) é o padrão de excelência para a comunicação de alta velocidade entre microsserviços. Em vez de montar requisições HTTP manuais com URLs, verbos e documentos de texto JSON, o gRPC permite que uma aplicação cliente invoque um método em um servidor remoto exatamente como se estivesse executando uma função nativa local do seu próprio código Go, com contratos rígidos, consumo mínimo de CPU e vazão multiplicada.


Mapa da aula

ParteTítuloPergunta central que responde
1O problema que o gRPC resolvePor que o modelo REST com JSON sobre HTTP/1.1 se torna um gargalo em arquiteturas de microsserviços internos?
2Os dois pilares: e HTTP/2O que torna o gRPC tão mais rápido e eficiente em consumo de banda e processamento de máquina?
3O contrato de serviço com Protocol BuffersComo escrever um arquivo .proto rigoroso e gerar código Go idiomático com o compilador protoc?
4Construindo o servidor gRPC em GoComo inicializar o listener TCP, registrar serviços, tratar requisições e realizar encerramento gracioso?
5Construindo o cliente gRPC em GoComo gerenciar conexões persistentes com grpc.NewClient e realizar chamadas seguras com prazos?
6Os quatro modos de comunicaçãoComo funcionam os fluxos unário, server streaming, client streaming e bidirecional na prática?
7Interceptors: o sistema de middlewaresComo interceptar requisições para logging estruturado, autenticação com metadados e métricas?
8Prazos, cancelamento e códigos de statusComo propagar prazos rígidos com context.Context e tratar erros idiomáticos da rede gRPC?
9Erros comuns e boas práticasQuais são as armadilhas clássicas de conexões, quebra de contrato e streaming em produção?
10Exercícios práticos com respostasComo testar e fixar o conhecimento de evolução de contratos, concorrência e interceptors?

1. O problema que o gRPC resolve

Durante anos, a arquitetura REST com payloads em JSON sobre o protocolo HTTP/1.1 foi a escolha padrão de quase toda a indústria de software. Para navegadores web e aplicativos móveis conversando com servidores públicos, o modelo funciona muito bem: o formato JSON é legível por humanos, qualquer linguagem de programação possui suporte nativo e os verbos HTTP mapeiam operações de banco de dados com clareza.

O problema surge quando abandonamos a borda da internet e olhamos para a comunicação interna de uma malha de microsserviços. Em uma arquitetura corporativa moderna, uma única solicitação de um usuário externo pode desencadear dezenas de chamadas internas em cadeia entre serviços de catálogo, estoque, pagamento, fraude e notificação. Quando todos esses serviços internos conversam entre si usando REST e JSON sobre HTTP/1.1, três gargalos graves de engenharia aparecem:

  1. Desperdício computacional de parsing de texto: O formato JSON é baseado em caracteres de texto puro. Cada requisição precisa serializar structs da memória em strings cheias de chaves repetidas (como "id", "nome_do_cliente", "preco"), enviar essas strings pela rede e, do outro lado, o servidor precisa varrer cada caractere para reconstruir as estruturas na memória. Isso queima ciclos valiosos de processamento da CPU apenas interpretando texto.
  2. Ausência de contratos estritos em tempo de compilação: Em APIs REST tradicionais, se o time responsável pelo microsserviço de pagamentos renomear o campo "amount" para "value", o microsserviço de checkout só descobrirá a quebra em tempo de execução, quando as requisições começarem a falhar silenciosamente com valores nulos.
  3. Bloqueio de linha e custo de conexões TCP no HTTP/1.1: O protocolo HTTP/1.1 opera no modelo estrito de uma requisição por vez por conexão física TCP. Se uma requisição for lenta, todas as outras na mesma conexão precisam esperar em fila (o fenômeno chamado de Head-of-Line Blocking no nível da aplicação). Para tentar contornar isso, os clientes abrem dezenas de conexões TCP simultâneas, consumindo portas, memória e tempo de handshake criptográfico.

O gRPC foi projetado pelo Google (como uma evolução aberta do seu sistema interno chamado Stubby) para erradicar esses três problemas de uma só vez.

rest vs grpc
figura 1 · rest vs grpc

Observe no diagrama acima a diferença visual entre as duas abordagens. Na parte superior, o cliente REST precisa abrir múltiplos canais TCP independentes, trafegando pacotes pesados de texto JSON com cabeçalhos redundantes e lentidão nas respostas. Na parte inferior, o cliente gRPC utiliza uma única conexão TCP duradoura, dividida internamente em múltiplos fluxos lógicos concorrentes (streams) que transmitem dados binários minúsculos em alta velocidade.


2. Os dois pilares: Protocol Buffers e HTTP/2

O desempenho do gRPC não é fruto de mágica, mas do casamento entre duas tecnologias projetadas para máxima eficiência: o mecanismo de serialização binária Protocol Buffers e o protocolo de transporte de rede HTTP/2.

O Protocol Buffers (Protobuf)

O Protocol Buffers é um mecanismo independente de linguagem e de plataforma criado pelo Google para serializar dados estruturados. Em vez de enviar o nome dos campos em texto (como "status": "APROVADO" em JSON), o Protobuf utiliza um contrato pré-compilado onde cada campo da mensagem recebe um número identificador inteiro (uma tag binária).

Quando o Go serializa uma mensagem com Protobuf, ele transmite pela rede apenas o identificador numérico daquele campo, o tipo de dado e o valor compactado em binário (usando técnicas de codificação como Varints). O receptor, que possui exatamente o mesmo contrato compilado, lê a tag numérica e sabe imediatamente em qual campo da struct Go aquela informação deve ser gravada. O resultado são mensagens que ocupam de um terço a um décimo do tamanho de um JSON equivalente, exigindo uma fração do esforço de CPU para serem decodificadas.

O transporte sobre HTTP/2

Diferente de conexões HTTP comuns, o gRPC utiliza o protocolo HTTP/2 como meio exclusivo de transporte. O HTTP/2 introduz três capacidades revolucionárias para a comunicação entre servidores:

  • verdadeira em conexão única: Uma única conexão TCP persistente entre o cliente e o servidor pode transportar centenas de chamadas RPC simultâneas. Cada chamada viaja em um fluxo lógico independente (um Stream ID numérico), eliminando a necessidade de abrir novas conexões a cada requisição.
  • Compressão de cabeçalhos com : Cabeçalhos repetitivos (como tokens de autenticação, agentes de usuário e caminhos) são indexados em tabelas estáticas e dinâmicas nas duas pontas, trafegando apenas referências mínimas de bytes.
  • Quadros binários (Binary Framing): O tráfego de rede deixa de ser texto livre e passa a ser dividido em pequenos quadros binários rígidos (como quadros de cabeçalho HEADERS e quadros de dados DATA), permitindo que a camada de transporte intercale pacotes de chamadas diferentes sem confusão.
anatomia grpc
figura 2 · anatomia grpc

Acompanhe o caminho traçado pelo diagrama acima da esquerda para a direita. O código Go do cliente invoca uma função no local como se fosse uma função comum da aplicação. O Stub entrega a struct Go para o serializador Protobuf, que converte os dados em uma sequência enxuta de bytes binários. O transporte HTTP/2 encapsula esses bytes em quadros com identificadores de fluxo e os transmite pela conexão TCP persistente. Ao atingir o servidor, os bytes são desserializados instantaneamente em uma struct Go idêntica, e o núcleo do servidor despacha a chamada para a função de negócio correspondente, devolvendo a resposta pelo mesmo caminho.


3. O contrato de serviço com Protocol Buffers

No desenvolvimento com gRPC, você nunca começa escrevendo código Go diretamente. O desenvolvimento começa obrigatoriamente pela especificação do contrato no arquivo de esquema com extensão .proto. O arquivo de contrato atua como a única fonte da verdade (single source of truth) compartilhada entre clientes e servidores, independentemente de qual linguagem cada um utilize.

Escrevendo o arquivo ecommerce.proto

Crie uma pasta chamada proto e adicione o arquivo ecommerce.proto:

syntax = "proto3";

package ecommerce.v1;

option go_package = "github.com/exemplo/ecommerce/proto/v1;ecommercev1";

// Mensagem que representa um produto no pedido
message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  double price = 3;
}

// Requisição de criação de pedido
message CreateOrderRequest {
  string customer_id = 1;
  repeated OrderItem items = 2;
}

// Resposta de criação de pedido
message CreateOrderResponse {
  string order_id = 1;
  string status = 2;
  int64 created_at = 3;
}

// Definição do serviço com seus métodos RPC
service OrderService {
  // Chamada unária clássica: 1 requisição -> 1 resposta
  rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
}

Observe alguns pontos fundamentais na sintaxe do Protocol Buffers:

  • syntax = "proto3";: declara que estamos usando a versão mais recente e padrão da especificação.
  • go_package: instrui o compilador sobre qual será o caminho completo do pacote Go gerado e o nome do pacote (ecommercev1).
  • repeated: indica que o campo é uma lista ou fatia (em Go, ele se tornará um slice []*OrderItem).
  • Os números = 1;, = 2;, = 3;: não são valores iniciais, mas sim os identificadores numéricos das tags binárias. O campo customer_id será representado no byte da rede como a tag 1.

Compilando o contrato com protoc

Para transformar o arquivo .proto em código Go executável, são necessários o compilador oficial protoc e dois plugins específicos mantidos pela equipe de Go:

# Instalação dos plugins de compilação para Go
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Com os plugins no seu PATH, execute a compilação:

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

Essa execução gera dois arquivos essenciais dentro da pasta proto/:

  1. ecommerce.pb.go: contém as structs Go equivalentes às mensagens Protobuf, com tags de reflexão, métodos de acesso (getters) defensivos contra ponteiros nulos e lógica de serialização binária.
  2. ecommerce_grpc.pb.go: contém a interface de servidor OrderServiceServer, o stub do cliente OrderServiceClient, o registro de handlers e a struct de retrocompatibilidade defensiva UnimplementedOrderServiceServer.

4. Construindo o servidor gRPC em Go

Com os stubs gerados, a implementação do servidor em Go consiste em criar uma struct que implemente os métodos definidos na interface do serviço e acoplá-la a um servidor gRPC ouvindo em uma porta TCP.

Implementação do servidor

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"os"
	"os/signal"
	"syscall"
	"time"

	pb "github.com/exemplo/ecommerce/proto/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// orderServer implementa a interface pb.OrderServiceServer gerada
type orderServer struct {
	pb.UnimplementedOrderServiceServer
}

// CreateOrder executa a lógica de criação do pedido
func (s *orderServer) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
	// Validação básica de entrada
	if req.GetCustomerId() == "" {
		return nil, status.Error(codes.InvalidArgument, "o identificador do cliente é obrigatório")
	}

	if len(req.GetItems()) == 0 {
		return nil, status.Error(codes.InvalidArgument, "o pedido deve conter ao menos um item")
	}

	// Simula a persistência e geração de identificador
	orderID := fmt.Sprintf("ord-%d", time.Now().UnixNano())
	log.Printf("[Servidor] Pedido %s criado com sucesso para o cliente %s com %d itens",
		orderID, req.GetCustomerId(), len(req.GetItems()))

	return &pb.CreateOrderResponse{
		OrderId:   orderID,
		Status:    "CRIADO",
		CreatedAt: time.Now().Unix(),
	}, nil
}

func main() {
	port := ":50051"
	listener, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("Falha ao abrir porta TCP %s: %v", port, err)
	}

	// Cria a instância do servidor gRPC
	grpcServer := grpc.NewServer()

	// Registra nossa implementação no servidor gRPC
	pb.RegisterOrderServiceServer(grpcServer, &orderServer{})

	// Canal para encerramento gracioso (Graceful Shutdown)
	stopChan := make(chan os.Signal, 1)
	signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM)

	go func() {
		log.Printf("Servidor gRPC escutando na porta %s...", port)
		if err := grpcServer.Serve(listener); err != nil && err != grpc.ErrServerStopped {
			log.Fatalf("Erro na execução do servidor: %v", err)
		}
	}()

	// Aguarda sinal de encerramento do sistema operacional
	<-stopChan
	log.Println("Sinal de parada recebido. Encerrando servidor graciosamente...")

	// Aguarda chamadas ativas terminarem antes de fechar o processo
	grpcServer.GracefulStop()
	log.Println("Servidor finalizado com segurança.")
}

Repare no uso de pb.UnimplementedOrderServiceServer. Embutir essa struct dentro de orderServer é uma convenção obrigatória do ecossistema Go para garantir que, se novos métodos forem adicionados ao contrato .proto no futuro, seu servidor continue compilando mesmo antes de você escrever a implementação de cada método novo.


5. Construindo o cliente gRPC em Go

Diferente de requisições HTTP REST em que bibliotecas comuns costumam criar conexões transitórias, um cliente gRPC foi feito para manter uma única conexão de longa duração com o servidor e reutilizá-la durante todo o ciclo de vida da aplicação.

Implementação do cliente

package main

import (
	"context"
	"log"
	"time"

	pb "github.com/exemplo/ecommerce/proto/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	serverAddr := "localhost:50051"

	// Cria o cliente de conexão persistente com credenciais inseguras (para desenvolvimento local)
	conn, err := grpc.NewClient(serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("Falha ao configurar conexão com %s: %v", serverAddr, err)
	}
	defer conn.Close()

	// Cria o Stub cliente a partir da conexão compartilhada
	client := pb.NewOrderServiceClient(conn)

	// Estabelece um prazo rígido (Deadline) para a chamada
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	// Monta a requisição tipada
	req := &pb.CreateOrderRequest{
		CustomerId: "cli-9872",
		Items: []*pb.OrderItem{
			{
				ProductId: "prod-teclado-mec",
				Quantity:  1,
				Price:     450.00,
			},
			{
				ProductId: "prod-mouse-gamer",
				Quantity:  2,
				Price:     180.00,
			},
		},
	}

	// Executa a chamada RPC como se fosse uma função local
	resp, err := client.CreateOrder(ctx, req)
	if err != nil {
		log.Fatalf("Erro ao criar pedido via gRPC: %v", err)
	}

	log.Printf("[Cliente] Sucesso! Pedido ID: %s | Status: %s | Timestamp: %d",
		resp.GetOrderId(), resp.GetStatus(), resp.GetCreatedAt())
}

O método grpc.NewClient() estabelece o canal de comunicação sem bloquear a execução do programa aguardando o servidor ligar. O gRPC gerencia nos bastidores a reconexão automática, balanceamento de carga e resolução de nomes.


6. Os quatro modos de comunicação

A maioria das pessoas associa chamadas de rede ao formato requisição e resposta. O gRPC vai muito além disso, suportando quatro padrões nativos de transmissão graças à flexibilidade dos fluxos de dados multiplexados do HTTP/2.

modos comunicacao
figura 3 · modos comunicacao

Analise no diagrama acima as quatro variações de fluxo. Cada uma delas atende a uma necessidade arquitetural específica do mundo dos sistemas distribuídos:

1. RPC Unário (Unary RPC)

O modelo tradicional: o cliente envia uma mensagem e aguarda o servidor processar e devolver exatamente uma mensagem de resposta. É ideal para consultas de saldo, autenticação, atualizações pontuais e operações CRUD padrão.

2. Fluxo do servidor (Server Streaming)

O cliente envia uma única requisição inicial e recebe de volta um fluxo contínuo e aberto de mensagens do servidor. O servidor pode enviar dezenas ou milhares de eventos ao longo de minutos e encerra a conexão apenas quando concluir o lote.

  • Casos de uso: Transmissão de cotações de ações em tempo real, download de relatórios volumosos divididos em fatias e exportação contínua de linhas de extrato financeiro.

3. Fluxo do cliente (Client Streaming)

O cliente abre a conexão e envia uma sequência contínua de mensagens para o servidor. Quando o cliente encerra o envio, o servidor processa tudo o que recebeu e devolve uma única mensagem consolidada.

  • Casos de uso: Envio de arquivos grandes em pedaços de bytes (chunks), upload de telemetria de sensores de IoT em lote e ingestão de métricas agregadas por minuto.

4. Fluxo bidirecional (Bidirectional Streaming)

O cliente e o servidor operam dois fluxos de mensagens independentes e assíncronos sobre a mesma conexão TCP. Ambos podem ler e escrever dados quando quiserem, sem necessidade de sincronização estrita de ordem.

  • Casos de uso: Sistemas de chat e colaboração em tempo real, sincronização de jogos multiplayer e protocolos de negociação de alta frequência em bolsas de valores.

Exemplo prático de streaming bidirecional em Go

Vejamos como o contrato de streaming bidirecional é modelado no arquivo .proto:

message ChatMessage {
  string user = 1;
  string message = 2;
  int64 timestamp = 3;
}

service ChatService {
  // A palavra-chave stream aparece tanto na entrada quanto na saída
  rpc LiveChat (stream ChatMessage) returns (stream ChatMessage);
}

No lado do servidor em Go, o método recebe uma interface de stream que permite ler (Recv()) e escrever (Send()) concorrentemente com goroutines:

func (s *chatServer) LiveChat(stream pb.ChatService_LiveChatServer) error {
	for {
		// Lê a próxima mensagem enviada pelo cliente
		msg, err := stream.Recv()
		if err == io.EOF {
			// O cliente finalizou o fluxo de envio dele
			return nil
		}
		if err != nil {
			return err
		}

		log.Printf("[Chat] Mensagem recebida de %s: %s", msg.GetUser(), msg.GetMessage())

		// Responde imediatamente pelo fluxo de saída do servidor
		reply := &pb.ChatMessage{
			User:      "Sistema Central",
			Message:   fmt.Sprintf("Eco: %s", msg.GetMessage()),
			Timestamp: time.Now().Unix(),
		}

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

7. Interceptors: o sistema de middlewares

Em qualquer aplicação de produção, tarefas transversais como autenticação de tokens, auditoria de requisições, métricas de latência e recuperação de pânicos não devem poluir a lógica de negócio dos seus handlers. No ecossistema HTTP comum chamamos isso de middleware. No gRPC, esse mecanismo se chama .

Existem duas categorias de interceptors no gRPC:

  1. Unary Interceptors: executados em volta de chamadas unárias simples.
  2. Stream Interceptors: executados em volta de fluxos contínuos de mensagens.
interceptors
figura 4 · interceptors

Acompanhe o ciclo no diagrama acima. Quando a chamada RPC do cliente atinge o servidor, ela entra primeiro no interceptor. O interceptor executa o pré-processamento (validando credenciais contidas nos metadados da requisição), enriquece o contexto com dados de auditoria, delega a execução para a função de negócio através de handler(ctx, req) e, quando a resposta retorna do banco ou do serviço interno, o interceptor retoma o controle no pós-processamento, medindo o tempo decorrido e registrando o status final antes de liberar a resposta para a rede.

Implementando um Unary Server Interceptor em Go

package main

import (
	"context"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

// LoggingAndAuthInterceptor atua como middleware de segurança e observabilidade
func LoggingAndAuthInterceptor(
	ctx context.Context,
	req any,
	info *grpc.UnaryServerInfo,
	handler grpc.UnaryHandler,
) (any, error) {
	start := time.Now()

	// 1. Extração e validação de metadados de autenticação
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, status.Error(codes.Unauthenticated, "metadados da requisição ausentes")
	}

	tokens := md.Get("authorization")
	if len(tokens) == 0 || tokens[0] != "Bearer token-secreto-123" {
		return nil, status.Error(codes.Unauthenticated, "token de autorização inválido ou ausente")
	}

	// 2. Execução da regra de negócio (passagem para o handler do serviço)
	resp, err := handler(ctx, req)

	// 3. Pós-processamento: cálculo de latência e logging estruturado
	duration := time.Since(start)
	statusCode := status.Code(err)

	log.Printf("[Interceptor] Método: %s | Duração: %v | Status: %s (%d)",
		info.FullMethod, duration, statusCode.String(), statusCode)

	return resp, err
}

Para acoplar o interceptor ao servidor, basta passá-lo como opção na inicialização:

grpcServer := grpc.NewServer(
    grpc.UnaryInterceptor(LoggingAndAuthInterceptor),
)

8. Prazos, cancelamento e códigos de status

Dois dos recursos mais poderosos do gRPC em ambientes distribuídos são a propagação automática de prazos (Deadlines) e o tratamento semântico de erros através de códigos canônicos de status.

O perigo da falta de prazos e o cancelamento em cascata

Em uma arquitetura de microsserviços, quando o Microsserviço A chama o Microsserviço B, que por sua vez chama o Microsserviço C, se o Microsserviço C travar e não houver um limite de tempo pré-definido, o Microsserviço B ficará com uma goroutine suspensa aguardando a resposta, o Microsserviço A também ficará bloqueado e o cliente final verá a tela de carregamento infinita. Em questão de minutos, todas as threads e recursos do cluster são esgotados.

O gRPC resolve isso de maneira elegante: ao definir um timeout usando context.WithTimeout(ctx, 500*time.Millisecond) no cliente, o gRPC serializa esse limite no cabeçalho HTTP/2 padronizado grpc-timeout. Conforme a requisição viaja de serviço em serviço, cada nó na cadeia sabe exatamente quanto tempo restante ainda possui para processar a tarefa.

deadlines cancelamento
figura 5 · deadlines cancelamento

Veja o que acontece no diagrama acima quando a operação no Microsserviço B demora mais do que os 500 milissegundos estipulados pelo cliente inicial. O relógio do contexto dispara o evento de cancelamento. Imediatamente, a conexão HTTP/2 envia um quadro de reset de fluxo (RST_STREAM), interrompendo a execução no Microsserviço B para não gastar CPU inutilmente e devolvendo o código de status padronizado DeadlineExceeded em cadeia até o cliente.

Verificando cancelamento dentro de laços em Go

Para que sua aplicação respeite o cancelamento e libere recursos rapidamente, tarefas longas ou laços de repetição devem sempre escutar o canal ctx.Done():

func (s *orderServer) ProcessHeavyTask(ctx context.Context, req *pb.TaskRequest) (*pb.TaskResponse, error) {
	for i := 0; i < 100; i++ {
		// Verifica se o cliente cancelou a requisição ou se o prazo expirou
		select {
		case <-ctx.Done():
			log.Println("[Servidor] O cliente desistiu ou o timeout foi atingido. Abortando tarefa.")
			return nil, status.Error(codes.Canceled, "operação cancelada pelo cliente")
		default:
			// Continua o trabalho normal
			time.Sleep(50 * time.Millisecond)
		}
	}
	return &pb.TaskResponse{Result: "Concluído"}, nil
}

Códigos de erro oficiais do gRPC

Nunca retorne erros de texto simples em gRPC via fmt.Errorf() ou errors.New(). O gRPC possui uma tabela universal de 16 códigos canônicos de status (definidos no pacote google.golang.org/grpc/codes). Os mais importantes em produção são:

CódigoSignificadoQuando usar
codes.OK (0)SucessoRetorno bem-sucedido da chamada
codes.Canceled (1)CanceladoO cliente abortou a chamada antes do fim
codes.InvalidArgument (3)Argumento inválidoDados obrigatórios ausentes ou payload fora do padrão
codes.DeadlineExceeded (4)Prazo excedidoO tempo limite estipulado no contexto expirou
codes.NotFound (5)Não encontradoO registro solicitado não existe no banco
codes.AlreadyExists (6)Já existeTentativa de criar um recurso com ID ou e-mail duplicado
codes.PermissionDenied (7)Permissão negadaUsuário autenticado, mas sem privilégios para a ação
codes.Unauthenticated (16)Não autenticadoToken ausente, expirado ou assinatura inválida
codes.Unavailable (14)IndisponívelO serviço de destino está temporariamente fora do ar

9. Erros comuns e boas práticas

Trabalhar com gRPC em produção exige cuidados que não costumam aparecer em tutoriais introdutórios. Abaixo estão os erros mais caros e frequentes cometidos em ambientes reais de produção:

  1. Abrir conexões a cada requisição:
    • O erro: Executar grpc.NewClient() ou grpc.Dial() dentro do handler de cada chamada que chega à sua API.
    • Por que é grave: Cada discagem realiza resolução de DNS, handshakes TCP e TLS e inicialização de janelas HTTP/2. Isso anula a maior vantagem do gRPC.
    • A solução: Crie a conexão uma única vez na inicialização da aplicação (na função main), compartilhe o ponteiro da conexão entre as structs de serviço e use defer conn.Close() apenas no encerramento do processo.
  2. Reutilizar números de tags antigas em arquivos .proto:
    • O erro: Remover um campo de texto antigo que tinha o número = 2; e criar um novo campo de número inteiro usando = 2;.
    • Por que é grave: O Protobuf identifica campos unicamente pelo número da tag. Clientes com versões antigas do binário tentarão ler dados numéricos como texto, gerando corrupção de dados ou falhas silenciosas difíceis de rastrear.
    • A solução: Nunca reutilize números de tags. Use reserved 2; para travar o identificador permanentemente.
  3. Não configurar timeouts nas chamadas:
    • O erro: Executar client.CreateOrder(context.Background(), req).
    • Por que é grave: Sem , se o servidor remoto enfrentar lentidão de rede, a requisição ficará aguardando indefinidamente, acumulando goroutines no cliente e eventualmente derrubando o sistema por esgotamento de memória.
    • A solução: Utilize sempre context.WithTimeout(ctx, duration).
  4. Ignorar o erro io.EOF em métodos de streaming:
    • O erro: Tratar qualquer retorno não nulo de stream.Recv() como uma falha fatal.
    • Por que é grave: Quando o outro lado encerra o envio ordenado das mensagens, a função Recv() retorna o erro sentinela io.EOF. Esse erro não significa pane, mas sim que o fluxo terminou normalmente.
    • A solução: Compare explicitamente if err == io.EOF { return nil }.
  5. Expor portas gRPC puras diretamente para navegadores web comuns:
    • O erro: Tentar consumir gRPC diretamente em uma página frontend com JavaScript tradicional.
    • Por que é grave: Os navegadores web não oferecem controle direto sobre quadros HTTP/2 e trailers HTTP.
    • A solução: Para comunicação com navegadores, utilize uma camada de proxy intermediária como o gRPC-Web (com o proxy Envoy) ou o utilitário grpc-gateway, que traduz requisições REST/JSON para chamadas gRPC internas automaticamente.

10. Exercícios práticos com respostas

Coloque o conhecimento à prova respondendo aos três exercícios abaixo antes de conferir o gabarito comentado.

Exercício 1: Modelagem defensiva de contrato

Você possui um microsserviço de usuários em produção com a seguinte mensagem Protobuf:

message UserProfile {
  string id = 1;
  string full_name = 2;
  string phone = 3;
}

A equipe decidiu que o campo phone deve ser descontinuado, pois agora teremos suporte a múltiplos números com DDD e código do país. Como você deve alterar este contrato para permitir que novos clientes enviem a lista estruturada sem quebrar os aplicativos móveis legados em produção que ainda utilizam o campo antigo?


Exercício 2: Detecção de vazamento de conexão

Um desenvolvedor júnior da sua equipe escreveu o seguinte trecho de código dentro de um handler HTTP que precisa consultar o saldo de um cliente via gRPC:

func HandleGetBalance(w http.ResponseWriter, r *http.Request) {
    conn, _ := grpc.NewClient("balance-service:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    client := pb.NewBalanceServiceClient(conn)
    resp, _ := client.GetBalance(r.Context(), &pb.BalanceRequest{UserId: "123"})
    fmt.Fprintf(w, "Saldo: %d", resp.GetAmount())
}

Sob tráfego de 500 requisições por segundo, o serviço começou a apresentar lentidão extrema e colapsou com o erro de sistema operacional too many open files. Aponte os dois erros capitais presentes no código e escreva a correção idiomática em Go.


Exercício 3: Implementando interceptor de segurança

Escreva a função de um grpc.UnaryServerInterceptor em Go que verifique se a requisição possui um cabeçalho de metadados chamado x-api-key. Se a chave for igual a "chave-mestra-xyz", a chamada deve prosseguir normalmente. Se a chave estiver ausente ou for diferente, o interceptor deve abortar a execução imediatamente e devolver o código de status canônico de permissão negada (codes.PermissionDenied).


Respostas comentadas dos exercícios

Resposta do Exercício 1

A evolução segura do contrato deve preservar o histórico e nunca reaproveitar a tag 3:

message PhoneNumber {
  string country_code = 1;
  string area_code = 2;
  string number = 3;
}

message UserProfile {
  string id = 1;
  string full_name = 2;
  // O campo antigo é reservado para garantir que ninguém utilize a tag 3 no futuro
  reserved 3;
  reserved "phone";

  // O novo campo recebe uma nova tag nunca antes utilizada
  repeated PhoneNumber phone_numbers = 4;
}

Por que isso é seguro: Clientes antigos continuarão funcionando sem quebras porque o Protobuf simplesmente ignora tags que ele não conhece no esquema dele. A cláusula reserved 3; impede que outro desenvolvedor declare um campo novo com a tag 3 por engano.


Resposta do Exercício 2

Os dois erros capitais são:

  1. Instanciação contínua de conexões TCP: Criar uma nova conexão com grpc.NewClient() a cada requisição HTTP esgota rapidamente os descritores de arquivo do sistema operacional (file descriptors), pois a conexão nunca é fechada e o overhead de handshakes derruba o servidor.
  2. Ignorar retornos de erro e encerramento: Os retornos de erro foram descartados com o caractere sublinhado (_), ocultando falhas de conexão.

Correção idiomática: A conexão e o stub devem ser inicializados uma única vez no início da aplicação e injetados na struct do handler:

type BalanceHandler struct {
    client pb.BalanceServiceClient
}

func NewBalanceHandler(client pb.BalanceServiceClient) *BalanceHandler {
    return &BalanceHandler{client: client}
}

func (h *BalanceHandler) HandleGetBalance(w http.ResponseWriter, r *http.Request) {
    // Utiliza o contexto com timeout derivado da requisição HTTP original
    ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
    defer cancel()

    resp, err := h.client.GetBalance(ctx, &pb.BalanceRequest{UserId: "123"})
    if err != nil {
        http.Error(w, "Falha ao obter saldo", http.StatusInternalServerError)
        return
    }

    fmt.Fprintf(w, "Saldo: %d", resp.GetAmount())
}

Resposta do Exercício 3

func APIKeyInterceptor(
	ctx context.Context,
	req any,
	info *grpc.UnaryServerInfo,
	handler grpc.UnaryHandler,
) (any, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, status.Error(codes.PermissionDenied, "metadados de autenticação ausentes")
	}

	apiKeys := md.Get("x-api-key")
	if len(apiKeys) == 0 || apiKeys[0] != "chave-mestra-xyz" {
		return nil, status.Error(codes.PermissionDenied, "chave de API inválida ou não autorizada")
	}

	// Chave válida: autoriza a execução do handler de negócio
	return handler(ctx, req)
}

Glossário

  • RPC (Remote Procedure Call): Paradigma de computação distribuída que permite a um programa de computador fazer com que uma função seja executada em outro espaço de endereçamento (comumente em outro computador compartilhado por rede) sem que o programador precise codificar explicitamente os detalhes da interação de rede.
  • Protocol Buffers (Protobuf): Mecanismo neutro e eficiente desenvolvido pelo Google para serialização compacta de dados estruturados em formato binário.
  • Stub: Código gerado automaticamente pelo compilador que atua como proxy do lado do cliente, empacotando os argumentos da chamada e enviando-os pela rede como se fosse a função de negócio real.
  • Multiplexação: Capacidade de transmitir múltiplos fluxos independentes de requisição e resposta simultaneamente através de uma única conexão de transporte TCP, sem interferência mútua.
  • HPACK: Algoritmo de compressão de cabeçalhos introduzido pelo HTTP/2 para eliminar redundância de tráfego de metadados na rede.
  • Varint: Método de serialização de números inteiros em um ou mais bytes, onde valores numéricos menores ocupam menos espaço físico na rede.
  • Interceptor: Função intermediária que intercepta a execução de chamadas RPC no cliente ou no servidor, análoga aos middlewares do padrão HTTP tradicional.
  • Metadata: Informações adicionais de contexto (como tokens de autenticação e identificadores de rastreamento) enviadas no início ou no fim de uma chamada RPC na forma de pares chave-valor nos cabeçalhos HTTP/2.
  • Deadline: Instante fixo no tempo até o qual uma operação distribuída pode ser executada antes de ser cancelada automaticamente por timeout.

Fontes e documentação consultada

  • Documentação Oficial do gRPC (grpc.io/docs/languages/go), consultada em 16 de outubro de 2026.
  • Especificação oficial de Protocol Buffers (protobuf.dev), consultada em 16 de outubro de 2026.
  • Repositório oficial do gRPC para a linguagem Go (github.com/grpc/grpc-go), consultado em 16 de outubro de 2026.
  • Especificação formal do protocolo HTTP/2 (RFC 7540 e RFC 9113), IETF.
  • Diretrizes de Engenharia e Boas Práticas de Sistemas Distribuídos do Google (Site Reliability Engineering / Building Secure and Reliable Systems).

Relacionados

  • Context: como propagar cancelamentos e prazos rígidos através de chamadas de rede em Go.
  • Goroutine: o modelo de concorrência leve de Go que alimenta os streams do gRPC.
  • Channels: a mecânica de sincronização e comunicação utilizada em implementações de streaming.
  • WebSocket: comparação entre conexões bidirecionais web comuns e o streaming nativo do gRPC.
  • Curriculo de um Desenvolvedor Go: o mapa completo de habilidades e salários para atuar com Go em sistemas distribuídos.
  • Microsserviços em Go: padrões de arquitetura de software para comunicação interna em larga escala.
  • Índice de Desafios Técnicos Go: desafios práticos de código de grandes empresas como Coinbase, Uber e Cloudflare.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?