O que é gRPC e Por Que Importa
gRPC é um framework de chamada de procedimento remoto (RPC) moderno, desenvolvido pelo Google, que permite que aplicações se comuniquem de forma eficiente através da rede. Diferentemente de REST, que usa HTTP/1.1 e serialização JSON, gRPC é construído sobre HTTP/2 e utiliza Protocol Buffers para serialização de dados. Essa combinação resulta em latência menor, melhor utilização de banda e suporte nativo para streaming bidirecional.
Em Go, gRPC é particularmente poderoso porque a linguagem foi projetada para lidar com concorrência de forma simples e elegante. Se você trabalha com microsserviços, APIs internas de alta performance ou sistemas que precisam comunicação em tempo real, gRPC é uma escolha técnica que justifica o investimento em aprendizado. A comunidade Go ao redor de gRPC é ativa, bem documentada e as bibliotecas disponíveis são maduras.
Protocol Buffers: A Base de Tudo
O que São Protocol Buffers
Protocol Buffers (protobuf) é uma linguagem de serialização independente de linguagem desenvolvida pelo Google. Você define suas estruturas de dados em arquivos .proto e ferramentas geram código para serialização e desserialização em qualquer linguagem. Ao contrário de JSON, os Protocol Buffers são binários, mais compactos e mais rápidos de processar.
Um arquivo .proto é a interface contratual entre cliente e servidor. Quando você modifica um .proto, precisa regenerar o código em ambos os lados. Essa abordagem garante compatibilidade, versionamento claro e documentação automática.
Estrutura Básica de um Arquivo Proto
Vamos criar um arquivo user.proto para um serviço simples de gerenciamento de usuários:
syntax = "proto3";
package user;
option go_package = "github.com/seu-usuario/seu-projeto/pb/user";
message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
message CreateUserRequest {
string name = 1;
string email = 2;
int32 age = 3;
}
message CreateUserResponse {
User user = 1;
string message = 2;
}
service UserService {
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc GetUser(User) returns (User);
}
A declaração syntax = "proto3" define a versão do Protocol Buffers (a mais atual e recomendada). O package organiza seu código gerado, e go_package especifica o caminho Go do pacote gerado. Cada campo tem um número único (= 1, = 2, etc.) que identifica aquele campo de forma binária — nunca reutilize esses números em versões futuras, pois isso quebra compatibilidade.
Gerando Código Go a Partir do Proto
Para converter seu arquivo .proto em código Go, você precisa instalar o compilador protoc e plugins Go:
# Instalar protoc (no macOS com Homebrew)
brew install protobuf
# Instalar 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
# Gerar código Go
protoc --go_out=. --go-grpc_out=. user.proto
Isso criará dois arquivos: user.pb.go (estruturas de dados) e user_grpc.pb.go (interfaces e clientes/servidores gRPC). Nunca edite esses arquivos manualmente — eles são gerados e serão sobrescritos na próxima execução do protoc.
Implementando um Serviço gRPC Básico
Servidor gRPC
Agora vamos implementar um servidor que satisfaça a interface gerada. Crie um arquivo server.go:
package main
import (
"context"
"fmt"
"log"
"net"
pb "github.com/seu-usuario/seu-projeto/pb/user"
"google.golang.org/grpc"
)
type userServer struct {
pb.UnimplementedUserServiceServer
users map[int32]*pb.User
nextID int32
}
func (s *userServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
s.nextID++
user := &pb.User{
Id: s.nextID,
Name: req.Name,
Email: req.Email,
Age: req.Age,
}
s.users[user.Id] = user
return &pb.CreateUserResponse{
User: user,
Message: "User created successfully",
}, nil
}
func (s *userServer) GetUser(ctx context.Context, req *pb.User) (*pb.User, error) {
user, ok := s.users[req.Id]
if !ok {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
func main() {
listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterUserServiceServer(grpcServer, &userServer{
users: make(map[int32]*pb.User),
nextID: 0,
})
fmt.Println("Server running on :50051")
if err := grpcServer.Serve(listener); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
O servidor escuta na porta 50051 (padrão para gRPC). A estrutura userServer implementa as interfaces geradas pelo protoc. Note que herdamos UnimplementedUserServiceServer para garantir compatibilidade futura se novos métodos forem adicionados ao serviço.
Cliente gRPC
Crie um arquivo client.go para testar:
package main
import (
"context"
"fmt"
"log"
"time"
pb "github.com/seu-usuario/seu-projeto/pb/user"
"google.golang.org/grpc"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
// Criar um usuário
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
createResp, err := client.CreateUser(ctx, &pb.CreateUserRequest{
Name: "João Silva",
Email: "joao@example.com",
Age: 28,
})
if err != nil {
log.Fatalf("CreateUser failed: %v", err)
}
fmt.Printf("Created user: %v\n", createResp.User)
// Buscar um usuário
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
defer cancel()
getResp, err := client.GetUser(ctx, &pb.User{Id: createResp.User.Id})
if err != nil {
log.Fatalf("GetUser failed: %v", err)
}
fmt.Printf("Retrieved user: %v\n", getResp)
}
O cliente cria uma conexão com o servidor usando grpc.Dial. Note o uso de context.WithTimeout para adicionar um deadline: se o servidor não responder em 1 segundo, a requisição é cancelada. Isso é essencial em sistemas distribuídos para evitar travamentos.
Streaming em gRPC
Tipos de Streaming
gRPC suporta quatro padrões de comunicação: unário (tradicional request-response), server streaming (servidor envia múltiplas mensagens), client streaming (cliente envia múltiplas mensagens) e bidirecional (ambos enviam múltiplas mensagens). Streaming é particularmente útil para dados em tempo real, download/upload de arquivos grandes ou processamento de fluxos de eventos.
Definindo Streaming no Proto
Vamos estender nosso arquivo user.proto com streaming:
syntax = "proto3";
package user;
option go_package = "github.com/seu-usuario/seu-projeto/pb/user";
message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
message CreateUserRequest {
string name = 1;
string email = 2;
int32 age = 3;
}
message CreateUserResponse {
User user = 1;
string message = 2;
}
message ListUsersRequest {
int32 limit = 1;
}
message UserEvent {
User user = 1;
string event_type = 2;
}
service UserService {
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc GetUser(User) returns (User);
rpc ListUsers(ListUsersRequest) returns (stream User);
rpc WatchUsers(stream User) returns (stream UserEvent);
}
A palavra-chave stream define que aquele parâmetro pode ter múltiplas mensagens. ListUsers é server streaming (cliente envia uma requisição, servidor envia múltiplos usuários). WatchUsers é bidirecional (ambos enviam múltiplas mensagens).
Implementando Server Streaming
Atualize seu server.go:
package main
import (
"context"
"fmt"
"log"
"net"
"time"
pb "github.com/seu-usuario/seu-projeto/pb/user"
"google.golang.org/grpc"
)
type userServer struct {
pb.UnimplementedUserServiceServer
users map[int32]*pb.User
nextID int32
}
func (s *userServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
s.nextID++
user := &pb.User{
Id: s.nextID,
Name: req.Name,
Email: req.Email,
Age: req.Age,
}
s.users[user.Id] = user
return &pb.CreateUserResponse{
User: user,
Message: "User created successfully",
}, nil
}
func (s *userServer) GetUser(ctx context.Context, req *pb.User) (*pb.User, error) {
user, ok := s.users[req.Id]
if !ok {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
func (s *userServer) ListUsers(req *pb.ListUsersRequest, stream grpc.ServerStream) error {
count := int32(0)
for _, user := range s.users {
if count >= req.Limit && req.Limit > 0 {
break
}
if err := stream.SendMsg(user); err != nil {
return err
}
count++
}
return nil
}
func (s *userServer) WatchUsers(stream grpc.ServerStream) error {
for {
user := &pb.User{}
if err := stream.RecvMsg(user); err != nil {
return err
}
event := &pb.UserEvent{
User: user,
EventType: "user_received",
}
if err := stream.SendMsg(event); err != nil {
return err
}
}
}
func main() {
listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterUserServiceServer(grpcServer, &userServer{
users: make(map[int32]*pb.User),
nextID: 0,
})
fmt.Println("Server running on :50051")
if err := grpcServer.Serve(listener); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
Em ListUsers, usamos stream.SendMsg() para enviar cada usuário. O cliente receberá todos os usuários em sequência. Em WatchUsers (bidirecional), usamos stream.RecvMsg() para receber mensagens do cliente e stream.SendMsg() para responder.
Cliente com Streaming
package main
import (
"context"
"fmt"
"log"
"time"
pb "github.com/seu-usuario/seu-projeto/pb/user"
"google.golang.org/grpc"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
// Server streaming: ListUsers
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
stream, err := client.ListUsers(ctx, &pb.ListUsersRequest{Limit: 10})
if err != nil {
log.Fatalf("ListUsers failed: %v", err)
}
for {
user, err := stream.Recv()
if err != nil {
fmt.Println("Stream finished")
break
}
fmt.Printf("Received user: %v\n", user)
}
// Bidirecional streaming: WatchUsers
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
stream2, err := client.WatchUsers(ctx)
if err != nil {
log.Fatalf("WatchUsers failed: %v", err)
}
// Enviar alguns usuários
go func() {
for i := 1; i <= 3; i++ {
stream2.Send(&pb.User{
Id: int32(i),
Name: fmt.Sprintf("User %d", i),
Email: fmt.Sprintf("user%d@example.com", i),
Age: 20 + int32(i),
})
time.Sleep(500 * time.Millisecond)
}
stream2.CloseSend()
}()
// Receber eventos
for {
event, err := stream2.Recv()
if err != nil {
fmt.Println("Watch finished")
break
}
fmt.Printf("Event: %v\n", event)
}
}
Em streaming de cliente, chamamos stream.Recv() em um loop para receber mensagens até que o stream termine (erro EOF). Para bidirecional, executamos o envio em uma goroutine paralela enquanto a goroutine principal recebe eventos.
Interceptors: Middleware Poderoso do gRPC
Entendendo Interceptors
Interceptors são como middleware HTTP — eles interceptam chamadas de RPC antes de chegar ao handler e depois que a resposta é gerada. Use interceptors para logging, autenticação, autorização, rate limiting, métricas e observabilidade. Existem unary interceptors (para chamadas normais) e stream interceptors (para streaming).
Implementando um Unary Interceptor
Um exemplo prático: um interceptor que loga todas as chamadas RPC:
package main
import (
"context"
"fmt"
"log"
"net"
"time"
pb "github.com/seu-usuario/seu-projeto/pb/user"
"google.golang.org/grpc"
)
type userServer struct {
pb.UnimplementedUserServiceServer
users map[int32]*pb.User
nextID int32
}
func (s *userServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
s.nextID++
user := &pb.User{
Id: s.nextID,
Name: req.Name,
Email: req.Email,
Age: req.Age,
}
s.users[user.Id] = user
return &pb.CreateUserResponse{
User: user,
Message: "User created successfully",
}, nil
}
func (s *userServer) GetUser(ctx context.Context, req *pb.User) (*pb.User, error) {
user, ok := s.users[req.Id]
if !ok {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
// Unary Interceptor para logging
func loggingUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
fmt.Printf("[%s] RPC iniciado: %s\n", time.Now().Format(time.RFC3339), info.FullMethod)
resp, err := handler(ctx, req)
duration := time.Since(start)
fmt.Printf("[%s] RPC finalizado: %s (duração: %v, erro: %v)\n",
time.Now().Format(time.RFC3339), info.FullMethod, duration, err)
return resp, err
}
func main() {
listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
// Criar servidor com interceptor
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(loggingUnaryInterceptor),
)
pb.RegisterUserServiceServer(grpcServer, &userServer{
users: make(map[int32]*pb.User),
nextID: 0,
})
fmt.Println("Server running on :50051")
if err := grpcServer.Serve(listener); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
O interceptor recebe o contexto, a requisição, informações do RPC e o handler (a função real que será executada). Ele pode inspecionar/modificar o contexto e requisição antes de chamar handler(), e processar a resposta/erro depois. Neste exemplo, medimos o tempo de execução.
Interceptor de Autenticação
Um exemplo mais prático: validar um token JWT:
package main
import (
"context"
"fmt"
"log"
"net"
"strings"
"time"
pb "github.com/seu-usuario/seu-projeto/pb/user"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
type userServer struct {
pb.UnimplementedUserServiceServer
users map[int32]*pb.User
nextID int32
}
func (s *userServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
s.nextID++
user := &pb.User{
Id: s.nextID,
Name: req.Name,
Email: req.Email,
Age: req.Age,
}
s.users[user.Id] = user
return &pb.CreateUserResponse{
User: user,
Message: "User created successfully",
}, nil
}
func (s *userServer) GetUser(ctx context.Context, req *pb.User) (*pb.User, error) {
user, ok := s.users[req.Id]
if !ok {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
// Interceptor de autenticação
func authUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, fmt.Errorf("missing metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, fmt.Errorf("missing authorization token")
}
token := tokens[0]
if !strings.HasPrefix(token, "Bearer ") {
return nil, fmt.Errorf("invalid token format")
}
token = strings.TrimPrefix(token, "Bearer ")
if token != "valid-secret-token" {
return nil, fmt.Errorf("invalid or expired token")
}
return handler(ctx, req)
}
func main() {
listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(authUnaryInterceptor),
)
pb.RegisterUserServiceServer(grpcServer, &userServer{
users: make(map[int32]*pb.User),
nextID: 0,
})
fmt.Println("Server running on :50051")
if err := grpcServer.Serve(listener); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
O servidor agora rejeita requisições sem um header authorization válido. O cliente precisa enviar esse header:
package main
import (
"context"
"fmt"
"log"
"time"
pb "github.com/seu-usuario/seu-projeto/pb/user"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
// Adicionar metadata (header) com token
ctx := context.Background()
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer valid-secret-token")
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
resp, err := client.CreateUser(ctx, &pb.CreateUserRequest{
Name: "João Silva",
Email: "joao@example.com",
Age: 28,
})
if err != nil {
log.Fatalf("CreateUser failed: %v", err)
}
fmt.Printf("Created user: %v\n", resp.User)
}
Stream Interceptor
Para streaming, use grpc.StreamInterceptor:
package main
import (
"context"
"fmt"
"log"
pb "github.com/seu-usuario/seu-projeto/pb/user"
"google.golang.org/grpc"
)
type userServer struct {
pb.UnimplementedUserServiceServer
users map[int32]*pb.User
nextID int32
}
func (s *userServer) ListUsers(req *pb.ListUsersRequest, stream grpc.ServerStream) error {
count := int32(0)
for _, user := range s.users {
if count >= req.Limit && req.Limit > 0 {
break
}
if err := stream.SendMsg(user); err != nil {
return err
}
count++
}
return nil
}
// Stream Interceptor para logging
func loggingStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
fmt.Printf("Stream iniciado: %s\n", info.FullMethod)
err := handler(srv, ss)
fmt.Printf("Stream finalizado: %s (erro: %v)\n", info.FullMethod, err)
return err
}
func main() {
listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
grpcServer := grpc.NewServer(
grpc.StreamInterceptor(loggingStreamInterceptor),
)
pb.RegisterUserServiceServer(grpcServer, &userServer{
users: make(map[int32]*pb.User),
nextID: 0,
})
fmt.Println("Server running on :50051")
if err := grpcServer.Serve(listener); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
Encadeando Múltiplos Interceptors
Se precisar de vários interceptors, use bibliotecas como go-grpc-middleware ou encadeie manualmente:
func chainUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
for i := len(interceptors) - 1; i >= 0; i-- {
next := handler
current := interceptors[i]
func(c grpc.UnaryServerInterceptor, n grpc.UnaryHandler) {
handler = func(ctx context.Context, req interface{}) (interface{}, error) {
return c(ctx, req, info, n)
}
}(current, next)
}
return handler(ctx, req)
}
}
// Uso
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(chainUnaryInterceptors(loggingUnaryInterceptor, authUnaryInterceptor)),
)
Na prática, use github.com/grpc-ecosystem/go-grpc-middleware que oferece várias implementações prontas e testadas.
Conclusão
Você aprendeu que gRPC com Protocol Buffers oferece uma base sólida para sistemas distribuídos modernos, fornecendo serialização eficiente, tipagem forte e contrato claro entre cliente e servidor. A configuração inicial pode parecer verbosa (definir .proto, gerar código, implementar servidores), mas essa estrutura traz muito mais manutenibilidade do que APIs REST desorganizadas.
O streaming em gRPC resolve casos de uso que seriam ineficientes em REST, como monitoramento em tempo real, upload/download de arquivos grandes e comunicação bidirecional. Server streaming e bidirecional são primitivas poderosas que emergem naturalmente da arquitetura HTTP/2 do gRPC.
Por fim, interceptors são a chave para adicionar funcionalidades transversais sem poluir o código de negócio, simplificando autenticação, logging, rate limiting e observabilidade. Combine Protocol Buffers bem definidos, streaming apropriado e interceptors bem estruturados, e você terá uma base sólida para microsserviços de produção em Go.