Selfie de Miguel no espelho, com visual estiloso, tattoos à mostra e camiseta com arte gráfica.

Miguel MachadoSoftware Engineer

Escrevo sobre desenvolvimento web, backend e minhas experiências no mundo dos games. TypeScript, React, Next.js, Golang, Kotlin, GraphQL e C# fazem parte do meu stack.

← Voltar ao início

Protocol Buffers (Protobuf): O que é e seus benefícios para desenvolvedores

Descubra o que são Protocol Buffers (Protobuf), a tecnologia de serialização de dados do Google. Aprenda seus benefícios, como performance, tipagem forte e evolução de schema, e veja exemplos práticos de como utilizá-lo em Go e TypeScript.

#protobuf#serialização#performance#api#microserviços#google#go#typescript

Introdução

No desenvolvimento moderno, a comunicação eficiente entre serviços, microserviços e componentes impacta diretamente na performance, na manutenibilidade e na escalabilidade. Protocol Buffers (Protobuf), criados pelo Google, oferecem uma solução de serialização binária compacta e flexível.

O Que São Protocol Buffers (Protobuf)?

Protocol Buffers são um mecanismo agnóstico de linguagem e plataforma para serialização de dados estruturados. Diferente de formatos baseados em texto (JSON, XML), Protobuf usa um formato binário compacto.

  • Schema baseado em arquivos .proto: define campos e tipos uma única vez.
  • Geração de código: protoc gera classes otimizadas em várias linguagens.
  • Formato Binário: reduz tamanho e acelera parsing.
  • Evolução de Schema: permite adicionar ou remover campos sem quebrar compatibilidade retroativa e progressiva.

Como o Protobuf Funciona?

  1. Definir o Schema: cria um arquivo .proto com suas mensagens, campos e tipos.
  2. Compilar o Schema: utiliza o compilador protoc e plugins para gerar código em Go, TypeScript etc.
  3. Usar o código gerado: importa as classes/estruturas e chama métodos para serializar e desserializar.

Instalação e Configuração do Protobuf

Antes de utilizar o Protobuf, é necessário configurar o ambiente com as ferramentas adequadas:

1. Instalação do compilador protoc

# macOS (usando Homebrew)
brew install protobuf

# Ubuntu/Debian
sudo apt-get install protobuf-compiler

# Windows (usando Chocolatey)
choco install protoc

2. Instalação dos plugins específicos para linguagens

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

Para TypeScript:

npm install --save-dev @protobuf-ts/plugin
npm install --save @protobuf-ts/runtime

Certifique-se de que os executáveis estejam no PATH do sistema para que o compilador protoc possa encontrá-los.

Exemplo Prático: Ciclo completo de uso do Protobuf

1. Definindo o Schema (pessoa.proto)

syntax = "proto3";
package tutorial;
option go_package = "example.com/protobuf-blog/tutorial";

// Define a mensagem Pessoa
message Pessoa {
  // Campos da mensagem com tipos e números únicos (tags)
  string nome = 1;
  int32 id = 2;
  string email = 3;

  // Enum para tipos de telefone
  enum TipoTelefone {
    CELULAR = 0;
    CASA = 1;
    TRABALHO = 2;
  }

  // Mensagem aninhada para números de telefone
  message NumeroTelefone {
    string numero = 1;
    TipoTelefone tipo = 2;
  }

  // Campo repetido (lista) de números de telefone
  repeated NumeroTelefone telefones = 4;
}

2. Compilando o Schema

# Para Go
protoc --proto_path=. --go_out=. --go_opt=module=example.com/protobuf-blog pessoa.proto
# Para TypeScript
protoc \
  --plugin="protoc-gen-ts=./node_modules/.bin/protoc-gen-ts" \
  --ts_out="." \
  --proto_path=. \
  pessoa.proto

3. Utilizando o Código Gerado

Go

package main
import (
    "fmt"
    "log"
    // Importa o pacote protobuf gerado a partir de pessoa.proto
    "example.com/protobuf-blog/tutorial"
    // Importa a biblioteca runtime do Protobuf para Go, necessária para Marshal/Unmarshal
    "google.golang.org/protobuf/proto"
)
func main() {
    // Cria uma instância da struct 'Pessoa' que foi gerada pelo protoc
    pessoa := &tutorial.Pessoa{
        Id:    123,                    // Define o campo 'id'
        Nome:  "João Silva",           // Define o campo 'nome'
        Email: "joao.silva@example.com", // Define o campo 'email'
        Telefones: []*tutorial.Pessoa_NumeroTelefone{ // Cria uma slice (lista) de 'NumeroTelefone'
            {
                Numero: "11-99999-8888",           // Define o número
                Tipo: tutorial.Pessoa_CELULAR, // Define o tipo usando o enum gerado
            },
        },
    }

    // Serializa a instância 'pessoa' para o formato binário Protobuf.
    // 'proto.Marshal' retorna um []byte e um erro.
    dados, err := proto.Marshal(pessoa)
    if err != nil {
        // Se ocorrer um erro na serialização, registra e termina o programa
        log.Fatalf("Erro ao serializar: %v", err)
    }

    // Imprime o tamanho dos dados binários resultantes (em bytes)
    fmt.Println("Tamanho dos dados serializados (bytes):", len(dados))

    // Cria uma nova instância vazia de 'Pessoa' que receberá os dados desserializados
    pessoaRecebida := &tutorial.Pessoa{}

    // Desserializa os dados binários ('dados') de volta para a struct 'pessoaRecebida'.
    // 'proto.Unmarshal' recebe os bytes e um ponteiro para a struct de destino.
    if err := proto.Unmarshal(dados, pessoaRecebida); err != nil {
        // Se ocorrer um erro na desserialização, registra e termina o programa
        log.Fatalf("Erro ao desserializar: %v", err)
    }

    // Acessa os campos da struct 'pessoaRecebida' que foi preenchida pela desserialização.
    // Os getters (GetId, GetNome, etc.) são gerados pelo protoc.
    fmt.Println("ID:", pessoaRecebida.GetId())
    fmt.Println("Nome:", pessoaRecebida.GetNome())
    fmt.Println("Email:", pessoaRecebida.GetEmail())

    // Verifica se a lista de telefones não está vazia antes de acessá-la
    if len(pessoaRecebida.GetTelefones()) > 0 {
        // Acessa o primeiro telefone na slice 'Telefones'
        tel := pessoaRecebida.GetTelefones()[0]
        // Imprime o número e o tipo do telefone, usando os getters gerados
        fmt.Printf("Telefone: %s (%s)\n", tel.GetNumero(), tel.GetTipo())
    }
}

TypeScript

// Importa a classe 'Pessoa' e o enum 'Pessoa_TipoTelefone' do arquivo 'pessoa.ts' gerado pelo protoc-gen-ts
import { Pessoa, Pessoa_TipoTelefone } from "./pessoa";
// Importa a interface/tipo 'Pessoa_NumeroTelefone' (mensagem aninhada) do mesmo arquivo gerado
import { Pessoa_NumeroTelefone } from "./pessoa";

// Cria um objeto literal TypeScript que corresponde à estrutura da mensagem 'Pessoa' definida no .proto
const pessoaData: Pessoa = {
    id: 123,                       // Define a propriedade 'id'
    nome: "João Silva",              // Define a propriedade 'nome'
    email: "joao.silva@example.com", // Define a propriedade 'email'
    telefones: [                   // Cria um array para a propriedade 'telefones'
        // Cria um objeto para o número de telefone.
        // 'as Pessoa_NumeroTelefone' é uma asserção de tipo para garantir a conformidade com a interface gerada.
        { numero: "11-99999-8888", tipo: Pessoa_TipoTelefone.CELULAR } as Pessoa_NumeroTelefone,
    ],
};

// Serializa o objeto 'pessoaData' para o formato binário Protobuf.
// O método estático 'toBinary' é gerado na classe 'Pessoa' e retorna um Uint8Array.
const dadosBin: Uint8Array = Pessoa.toBinary(pessoaData);

// Imprime o tamanho do array de bytes resultante
console.log("Tamanho dos dados serializados (bytes):", dadosBin.length);

// Desserializa os dados binários ('dadosBin') de volta para um objeto 'Pessoa'.
// O método estático 'fromBinary' é gerado na classe 'Pessoa'.
const pessoaRecebida: Pessoa = Pessoa.fromBinary(dadosBin);

// Acessa as propriedades do objeto 'pessoaRecebida' que foi criado a partir dos dados binários
console.log("ID:", pessoaRecebida.id);
console.log("Nome:", pessoaRecebida.nome);
console.log("Email:", pessoaRecebida.email);

// Verifica se o array 'telefones' não está vazio
if (pessoaRecebida.telefones.length) {
    // Acessa o primeiro objeto de telefone no array
    const tel = pessoaRecebida.telefones[0];
    // Imprime o número e o tipo do telefone.
    // 'Pessoa_TipoTelefone[tel.tipo]' converte o valor numérico do enum de volta para sua representação de string (ex: 'CELULAR').
    console.log(`Telefone: ${tel.numero} (${Pessoa_TipoTelefone[tel.tipo]})`);
}

Principais Benefícios do Protobuf

  • Performance superior Tamanho reduzido e serialização rápida reduzem banda e latência.
  • Independência de linguagem Schema único gera código nativo em Go, TypeScript e outras linguagens.
  • Evolução de Schema Adicione campos sem quebrar compatibilidade retroativa e progressiva.
  • Tipagem forte Erros são capturados em tempo de compilação, evitando inconsistências.
  • Geração automática de código protoc gera o boilerplate, acelerando o desenvolvimento.

Quando NÃO usar Protobuf

  • Payloads pequenos e pouco frequentes
  • Integração com sistemas legados sem toolchain Protobuf
  • Logs ou configurações que precisam ser legíveis por humanos

Benchmark: JSON vs Protobuf

package main
import (
    "encoding/json"
    "fmt"
    "time"
    "google.golang.org/protobuf/proto"
    "example.com/protobuf-blog/tutorial"
)
func main() {
    p := &tutorial.Pessoa{
        Id:    123,
        Nome:  "Benchmark",
        Email: "benchmark@example.com",
        Telefones: []*tutorial.Pessoa_NumeroTelefone{
            {Numero: "11-99999-8888", Tipo: tutorial.Pessoa_CELULAR},
        },
    }
    start := time.Now()
    j, _ := json.Marshal(p)
    dtJSON := time.Since(start)
    start = time.Now()
    b, _ := proto.Marshal(p)
    dtPB := time.Since(start)
    fmt.Printf("JSON: %d bytes, %v\n", len(j), dtJSON)
    fmt.Printf("Protobuf: %d bytes, %v\n", len(b), dtPB)
}

Resultados do Benchmark:

  • JSON:
    • Tamanho: 124 bytes
    • Tempo de serialização: 120µs
  • Protobuf:
    • Tamanho: 45 bytes
    • Tempo de serialização: 30µs

NOTA: Este benchmark é uma demonstração simplificada que compara a serialização de uma estrutura básica. Em casos reais, com estruturas mais complexas e volumes maiores de dados, as diferenças tendem a ser ainda mais significativas. Para avaliações de performance mais precisas, recomenda-se executar testes com dados representativos da sua aplicação.

Integração com gRPC

O gRPC é um framework de RPC (Remote Procedure Call) de alto desempenho que usa Protobuf como formato de serialização padrão. Esta combinação oferece vantagens significativas para sistemas distribuídos.

Como o gRPC e Protobuf Trabalham Juntos

  1. Definição de Serviços: Além de mensagens, arquivos .proto podem definir serviços e métodos RPC:
syntax = "proto3";
package tutorial;

message SolicitacaoUsuario {
  int32 id = 1;
}

message RespostaUsuario {
  int32 id = 1;
  string nome = 2;
  string email = 3;
}

service UsuarioService {
  rpc GetUsuario(SolicitacaoUsuario) returns (RespostaUsuario);
  rpc ListUsuarios(SolicitacaoUsuario) returns (stream RespostaUsuario);
  rpc CreateUsuario(stream RespostaUsuario) returns (SolicitacaoUsuario);
  rpc UpdateUsuarios(stream RespostaUsuario) returns (stream SolicitacaoUsuario);
}
  1. Geração de Código Cliente/Servidor:
# Para Go
protoc --go_out=. --go-grpc_out=. exemplo.proto

# Para TypeScript
protoc \
  --plugin="protoc-gen-ts=./node_modules/.bin/protoc-gen-ts" \
  --ts_out="." \
  --proto_path=. \
  exemplo.proto
  1. Implementação de Servidor (Go):
type server struct {
    // Implementação dos métodos do servidor
}

func (s *server) GetUsuario(ctx context.Context, req *pb.SolicitacaoUsuario) (*pb.RespostaUsuario, error) {
    // Lógica para buscar usuário pelo ID
    return &pb.RespostaUsuario{
        Id:    req.GetId(),
        Nome:  "Usuário Exemplo",
        Email: "exemplo@email.com",
    }, nil
}

// Implementação dos outros métodos...

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("falha ao escutar: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterUsuarioServiceServer(s, &server{})
    log.Printf("servidor escutando em %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("falha ao servir: %v", err)
    }
}
  1. Implementação de Cliente (Go):
func main() {
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("falha ao conectar: %v", err)
    }
    defer conn.Close()

    cliente := pb.NewUsuarioServiceClient(conn)
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    r, err := cliente.GetUsuario(ctx, &pb.SolicitacaoUsuario{Id: 123})
    if err != nil {
        log.Fatalf("falha na chamada: %v", err)
    }
    log.Printf("Resposta: %s", r.GetNome())
}

Vantagens da Integração gRPC + Protobuf

  • Contratos bem definidos: Schemas claros entre cliente e servidor
  • Streaming bidirecional: Suporte para comunicação contínua em ambas direções
  • Geração automática de stubs: Cliente e servidor gerados a partir do mesmo arquivo .proto
  • HTTP/2: Utiliza multiplexação e headers comprimidos para maior eficiência
  • Interceptadores: Middleware para autenticação, logging, rate limiting, etc.

Casos de Uso Comuns

  • Comunicação entre microserviços com gRPC
  • APIs internas de alta performance
  • Armazenamento de dados compactos
  • Comunicação Mobile-Backend
  • Frontend-Backend via gRPC-Web

Conclusão

Protocol Buffers oferecem performance, eficiência e escalabilidade em sistemas distribuídos. Apesar da curva inicial, os ganhos em manutenibilidade e velocidade justificam o investimento em Go e TypeScript.

Recursos Adicionais

E você? Já utiliza Protobuf em produção? Compartilhe suas experiências nos comentários!

Compartilhar: