Se voce ja trabalhou com APIs REST em Go, sabe que JSON sobre HTTP funciona bem para a maioria dos casos. Mas quando seus microsservicos precisam trocar milhares de mensagens por segundo com baixa latencia, o overhead de serializar JSON, parsear headers HTTP e manter conexoes stateless se torna um gargalo real.
gRPC resolve isso com Protocol Buffers (binario, compacto), HTTP/2 (multiplexing, streaming) e geracao automatica de codigo. Nao por acaso, ferramentas escritas em Go como Kubernetes, Istio, CockroachDB e etcd usam gRPC internamente.
O que E gRPC
gRPC e um framework de chamada remota de procedimento (RPC) criado pelo Google. Em vez de definir endpoints REST com verbos HTTP, voce define servicos e metodos em arquivos .proto, e o compilador gera o codigo cliente e servidor automaticamente.
As vantagens principais sobre REST:
- Protocol Buffers – formato binario ate 10x menor que JSON
- HTTP/2 nativo – multiplexing de requisicoes na mesma conexao
- Contrato forte – o
.protoe a fonte de verdade; cliente e servidor sempre concordam - Streaming – suporte nativo a streaming bidirecional
- Geracao de codigo – stubs gerados para Go, Python, Java, Rust e dezenas de linguagens
Definindo o Servico com Protocol Buffers
Tudo comeca com um arquivo .proto. Vamos criar um servico de gerenciamento de tarefas:
// proto/tasks/v1/tasks.proto
syntax = "proto3";
package tasks.v1;
option go_package = "github.com/seu-projeto/gen/tasks/v1;tasksv1";
service TaskService {
// Unary: cria uma tarefa
rpc CreateTask(CreateTaskRequest) returns (CreateTaskResponse);
// Unary: busca uma tarefa por ID
rpc GetTask(GetTaskRequest) returns (GetTaskResponse);
// Server streaming: lista tarefas em tempo real
rpc ListTasks(ListTasksRequest) returns (stream Task);
}
message Task {
string id = 1;
string title = 2;
string description = 3;
bool completed = 4;
int64 created_at = 5;
}
message CreateTaskRequest {
string title = 1;
string description = 2;
}
message CreateTaskResponse {
Task task = 1;
}
message GetTaskRequest {
string id = 1;
}
message GetTaskResponse {
Task task = 1;
}
message ListTasksRequest {
bool only_pending = 1;
}
Compile com protoc para gerar o codigo Go:
protoc --go_out=. --go-grpc_out=. proto/tasks/v1/tasks.proto
Isso gera dois arquivos: tasks.pb.go (mensagens) e tasks_grpc.pb.go (interface do servico).
Criando o Servidor gRPC em Go
Com o codigo gerado, implemente a interface do servico:
package main
import (
"context"
"fmt"
"log"
"net"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
tasksv1 "github.com/seu-projeto/gen/tasks/v1"
)
type taskServer struct {
tasksv1.UnimplementedTaskServiceServer
mu sync.RWMutex
tasks map[string]*tasksv1.Task
}
func newTaskServer() *taskServer {
return &taskServer{
tasks: make(map[string]*tasksv1.Task),
}
}
func (s *taskServer) CreateTask(
ctx context.Context,
req *tasksv1.CreateTaskRequest,
) (*tasksv1.CreateTaskResponse, error) {
if req.GetTitle() == "" {
return nil, status.Error(codes.InvalidArgument, "titulo e obrigatorio")
}
task := &tasksv1.Task{
Id: fmt.Sprintf("task-%d", time.Now().UnixNano()),
Title: req.GetTitle(),
Description: req.GetDescription(),
Completed: false,
CreatedAt: time.Now().Unix(),
}
s.mu.Lock()
s.tasks[task.Id] = task
s.mu.Unlock()
return &tasksv1.CreateTaskResponse{Task: task}, nil
}
func (s *taskServer) GetTask(
ctx context.Context,
req *tasksv1.GetTaskRequest,
) (*tasksv1.GetTaskResponse, error) {
s.mu.RLock()
task, ok := s.tasks[req.GetId()]
s.mu.RUnlock()
if !ok {
return nil, status.Error(codes.NotFound, "tarefa nao encontrada")
}
return &tasksv1.GetTaskResponse{Task: task}, nil
}
// Server streaming: envia tarefas uma a uma
func (s *taskServer) ListTasks(
req *tasksv1.ListTasksRequest,
stream tasksv1.TaskService_ListTasksServer,
) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, task := range s.tasks {
if req.GetOnlyPending() && task.Completed {
continue
}
if err := stream.Send(task); err != nil {
return err
}
}
return nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("erro ao escutar: %v", err)
}
srv := grpc.NewServer()
tasksv1.RegisterTaskServiceServer(srv, newTaskServer())
log.Println("servidor gRPC rodando em :50051")
if err := srv.Serve(lis); err != nil {
log.Fatalf("erro ao servir: %v", err)
}
}
Note o uso de sync.RWMutex para acesso concorrente seguro ao mapa – um padrao essencial em Go que exploramos no artigo sobre padroes de concorrencia.
Criando o Cliente gRPC
O cliente usa os stubs gerados para chamar o servidor como se fossem funcoes locais:
package main
import (
"context"
"fmt"
"io"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
tasksv1 "github.com/seu-projeto/gen/tasks/v1"
)
func main() {
conn, err := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("erro ao conectar: %v", err)
}
defer conn.Close()
client := tasksv1.NewTaskServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Criar uma tarefa
resp, err := client.CreateTask(ctx, &tasksv1.CreateTaskRequest{
Title: "Estudar gRPC em Go",
Description: "Implementar servico completo com streaming",
})
if err != nil {
log.Fatalf("erro ao criar tarefa: %v", err)
}
fmt.Printf("Tarefa criada: %s\n", resp.Task.Id)
// Listar tarefas via server streaming
stream, err := client.ListTasks(ctx, &tasksv1.ListTasksRequest{
OnlyPending: true,
})
if err != nil {
log.Fatalf("erro ao listar: %v", err)
}
for {
task, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("erro no stream: %v", err)
}
fmt.Printf("Tarefa: %s - %s\n", task.Id, task.Title)
}
}
O uso correto de context com timeout e essencial em clientes gRPC para evitar que chamadas fiquem penduradas indefinidamente.
Tipos de Streaming no gRPC
gRPC oferece quatro padroes de comunicacao:
| Padrao | Descricao | Uso Tipico |
|---|---|---|
| Unary | 1 request, 1 response | CRUD simples |
| Server streaming | 1 request, N responses | Listas grandes, feeds |
| Client streaming | N requests, 1 response | Upload de dados, agregacao |
| Bidirectional | N requests, N responses | Chat, real-time sync |
Exemplo de streaming bidirecional – um chat simples:
// No .proto:
// rpc Chat(stream ChatMessage) returns (stream ChatMessage);
func (s *chatServer) Chat(stream pb.ChatService_ChatServer) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
// Ecoa a mensagem de volta (em producao, faria broadcast)
reply := &pb.ChatMessage{
User: "servidor",
Content: fmt.Sprintf("Recebi: %s", msg.Content),
}
if err := stream.Send(reply); err != nil {
return err
}
}
}
Error Handling com Status Codes
gRPC tem seu proprio sistema de codigos de erro, mapeados de forma semantica:
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Recurso nao encontrado
return nil, status.Error(codes.NotFound, "tarefa nao encontrada")
// Argumento invalido
return nil, status.Error(codes.InvalidArgument, "titulo nao pode ser vazio")
// Erro interno
return nil, status.Errorf(codes.Internal, "erro no banco: %v", err)
// Deadline excedido (automatico quando context expira)
// codes.DeadlineExceeded
// Nao autenticado
return nil, status.Error(codes.Unauthenticated, "token invalido")
Essa abordagem estruturada para erros complementa o que discutimos sobre tratamento de erros em Go – o gRPC adiciona uma camada semantica com codigos padronizados.
gRPC vs REST: Quando Usar Cada Um
| Criterio | gRPC | REST |
|---|---|---|
| Formato | Protocol Buffers (binario) | JSON (texto) |
| Protocolo | HTTP/2 | HTTP/1.1 ou HTTP/2 |
| Streaming | Nativo (4 tipos) | Limitado (SSE, WebSocket) |
| Contrato | .proto (forte) | OpenAPI (opcional) |
| Browser | Precisa de grpc-web | Nativo |
| Performance | Superior em throughput | Suficiente para maioria |
| Debugging | Precisa de ferramentas (grpcurl) | curl, browser |
| Curva de aprendizado | Maior | Menor |
Use gRPC quando: comunicacao entre microsservicos internos, alta performance e necessario, streaming bidirecional, contratos fortes entre times.
Use REST quando: APIs publicas, integracao com browsers, simplicidade e prioridade, equipe pequena.
Na pratica, muitos sistemas usam ambos: gRPC internamente entre servicos e REST/HTMX na borda para clientes web.
Testando Servicos gRPC
Use grpcurl para testar manualmente:
# Listar servicos
grpcurl -plaintext localhost:50051 list
# Chamar metodo
grpcurl -plaintext -d '{"title": "Nova tarefa", "description": "Teste"}' \
localhost:50051 tasks.v1.TaskService/CreateTask
Para testes automatizados em Go, use bufconn para testar sem abrir uma porta real:
func TestCreateTask(t *testing.T) {
// bufconn cria um listener in-memory
lis := bufconn.Listen(1024 * 1024)
srv := grpc.NewServer()
tasksv1.RegisterTaskServiceServer(srv, newTaskServer())
go func() {
if err := srv.Serve(lis); err != nil {
t.Fatalf("erro ao servir: %v", err)
}
}()
defer srv.Stop()
// Conecta via bufconn
conn, err := grpc.NewClient(
"passthrough:///bufnet",
grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
return lis.DialContext(ctx)
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
t.Fatalf("erro ao conectar: %v", err)
}
defer conn.Close()
client := tasksv1.NewTaskServiceClient(conn)
resp, err := client.CreateTask(context.Background(), &tasksv1.CreateTaskRequest{
Title: "Tarefa de teste",
})
if err != nil {
t.Fatalf("CreateTask falhou: %v", err)
}
if resp.Task.Title != "Tarefa de teste" {
t.Errorf("titulo esperado 'Tarefa de teste', got '%s'", resp.Task.Title)
}
}
Esse padrao de teste in-memory e muito mais rapido que subir um servidor real. Para tecnicas avancadas de teste, veja nosso guia de fuzzing em Go.
Casos de Uso Reais
Projetos escritos em Go que usam gRPC extensivamente:
- Kubernetes – toda comunicacao entre API server, kubelet e controller managers
- Istio – service mesh com controle de trafego via gRPC
- CockroachDB – banco de dados distribuido com replicacao via gRPC
- etcd – armazenamento chave-valor distribuido usado pelo Kubernetes
- Vitess – sharding de MySQL usado pelo YouTube
Se voce esta construindo microsservicos em Go, gRPC e a escolha natural para comunicacao interna. Combine com logging estruturado e observabilidade para um sistema robusto em producao.
FAQ
gRPC funciona com browsers? Nao diretamente. Browsers nao suportam HTTP/2 trailers que gRPC usa. A solucao e usar grpc-web ou um proxy como Envoy que traduz entre gRPC e HTTP/1.1.
Preciso usar Protocol Buffers com gRPC? Tecnicamente nao – gRPC suporta outros codecs. Mas Protocol Buffers e o padrao e oferece a melhor performance e tooling. Nao ha razao pratica para usar outro formato.
Como fazer autenticacao em gRPC? Use interceptors (middlewares). O padrao mais comum e enviar tokens JWT via metadata (equivalente a headers HTTP). Veja nosso tutorial de autenticacao JWT em Go para entender o fluxo.
gRPC e mais rapido que REST? Em benchmarks tipicos, gRPC e 2-10x mais rapido para serializar/deserializar mensagens e usa menos banda. A diferenca e mais significativa com mensagens grandes ou alta frequencia de chamadas.
Como versionar APIs gRPC?
Use packages versionados no .proto (ex: tasks.v1, tasks.v2). Protocol Buffers tem regras claras de compatibilidade: nunca reutilize numeros de campo, adicione campos novos com numeros novos, e mantenha servicos antigos rodando durante a migracao.
Pronto para criar CLIs profissionais em Go? No proximo artigo, mostramos como usar Cobra e Viper para construir ferramentas de linha de comando com a mesma qualidade de kubectl e Docker CLI.