Skip to content

Protobuf

Site oficial: Protocol Buffers | Google Developers

Introdução

Tutorial oficial: Protocol Buffer Basics: Go | Protocol Buffers | Google Developers

Protocol Buffers é um mecanismo de serialização de dados estruturados extensível, independente de linguagem e protocolo, aberto pela Google em 2008, é mais rápido ao desempacotar e empacotar, muito usado em áreas de comunicação RPC, pode definir a forma estruturada dos dados, e então pode usar código fonte especialmente gerado para facilmente escrever e ler dados estruturados de e para vários fluxos de dados, e usar em várias linguagens, sobre Protocol Buffers será referido como protobuf neste texto.

protobuf é relativamente popular, especialmente na área go, gRPC o usa como mecanismo de serialização para transmissão de protocolo.

Sintaxe

Primeiro, vejamos um exemplo para ver como um arquivo protobuf se parece, em geral, sua sintaxe é muito simples, pode-se dominar em dez minutos. Abaixo está um exemplo de um arquivo chamado search.proto, a extensão de arquivo protobuf é .proto.

protobuf
syntax = "proto3";

message SearchRequest {
  string query = 1;
  string number = 2;
}

message SearchResult {
  string data = 1;
}

service SearchService {
  rpc Search(SearchRequest) returns(SearchResult);
}
  • A primeira linha syntax = "proto3"; indica o uso da sintaxe proto3, por padrão usa a sintaxe proto3.
  • message é declarado de forma similar a uma struct, é a estrutura básica em proto
  • SearchRequest define três campos, cada campo terá nome e tipo
  • service define um serviço, um serviço contém um ou mais interfaces rpc
  • A interface rpc deve ter exatamente um parâmetro e valor de retorno, seus tipos devem ser message, não podem ser tipos básicos.

Além disso, note que cada linha em um arquivo proto deve terminar com ponto e vírgula.

Comentários

O estilo de comentários é exatamente o mesmo que em go.

protobuf
syntax = "proto3";

/* Comentário
 * Comentário */
message SearchRequest {
  string query = 1; //Comentário
  string number = 2;
}

Tipos

Os modificadores de tipo só podem aparecer em message, não podem aparecer sozinhos.

Tipos Básicos

Tipo ProtoTipo Go
doublefloat64
floatfloat32
int32int32
int64int64
uint32uint32
uint64uint64
sint32int32
sint64int64
fixed32uint32
fixed64uint64
sfixed32int32
sfixed64int64
boolbool
stringstring
bytes[]byte

Arrays

Adicione o modificador repeated antes de um tipo básico para indicar que é um tipo de array, corresponde a slice em go.

protobuf
message Company {
  repeated string employee = 1;
}

Map

Definir tipo map em protobuf é formatado como segue

map<key_type, value_type> map_field = N;

key_type deve ser numérico ou string, value_type não tem restrição de tipo, veja um exemplo

protobuf
message Person {
  map<string, int64> cards = 1;
}

Campos

De fato, proto não é um tipo tradicional de chave-valor, no arquivo proto declarado não aparecerão dados específicos, após cada = do campo deve seguir o número único atual em message, estes números são usados para identificar e definir estes campos no corpo da mensagem binária. Os números começam em 1, números de 1-15 ocupam 1 byte, 16-2047 ocupam dois bytes, portanto, atribua o máximo possível os campos que aparecem frequentemente com números de 1-15 para economizar espaço, e deve deixar algum espaço para campos que podem aparecer frequentemente no futuro.

Os campos em uma message devem seguir as seguintes regras

  • singular: por padrão é o campo deste tipo, em uma message bem estruturada, só pode haver 0 ou 1 deste campo, ou seja, não pode haver o mesmo campo repetido. A declaração abaixo causará erro.

    protobuf
    syntax = "proto3";
    
    message SearchRequest {
      string query = 1;
      string number = 2;
      string number = 3;//Campo repetido
    }
  • optional: Similar a singular, apenas pode verificar explicitamente se o valor do campo foi definido, pode haver as seguintes duas situações

    • set: Será serializado
    • unset: Não será serializado
  • repeated: Este tipo de campo pode aparecer 0 ou múltiplas vezes, manterá valores repetidos em ordem (basicamente é um array, pode permitir que o mesmo tipo de valor apareça múltiplas vezes, e mantém na ordem de aparição, é o índice)

  • map: Campo do tipo par chave-valor, declarado como segue

    protobuf
    map<string,int32> config = 3;

Campos Reservados

A palavra-chave reserve pode declarar campos reservados, após declarar o número do campo reservado, não poderá mais ser usado como número e nome de outros campos, e ocorrerá erro durante a compilação. A resposta oficial do Google é: se um arquivo proto remover alguns números em uma nova versão, então no futuro outros usuários podem reutilizar estes números que foram removidos, mas se voltar para a versão antiga dos números causará inconsistência entre o campo correspondente e o número gerando erro, campos reservados podem servir como um lembrete no período de compilação, lembrando que não pode usar este campo de uso reservado, caso contrário a compilação não passará.

protobuf
syntax = "proto3";

message SearchRequest {
  string query = 1;
  string number = 2;
  map<string, int32> config = 3;
  repeated string a = 4;
  reserved "a"; //Declarar campo de nome específico como campo reservado
  reserved 1 to 2; //Declarar uma sequência de números como campo reservado
  reserved 3,4; //Declarar
}

Desta forma, este arquivo não passará na compilação.

Campos Obsoletos

Se um campo for obsoleto, pode ser escrito como segue.

protobuf
message Body {
  string name = 1 [deprecated = true];
}

Enums

Pode-se declarar constantes enum e usá-las como tipo de campo, note que o primeiro elemento do item enum deve ser zero, porque o valor padrão do item enum é o primeiro elemento.

protobuf
syntax = "proto3";

enum Type {
  GET = 0;
  POST = 1;
  PUT = 2;
  DELETE = 3;
}

message SearchRequest {
  string query = 1;
  string number = 2;
  map<string, int32> config = 3;
  repeated string a = 4;
  Type type = 5;
}

Quando houver itens enum com o mesmo valor dentro do item enum, pode-se usar alias de enum

protobuf
syntax = "proto3";

enum Type {
  option allow_alias = true; //Precisa habilitar a opção de configuração para permitir uso de alias
  GET = 0;
  GET_ALIAS = 0; //Alias do item enum GET
  POST = 1;
  PUT = 2;
  DELETE = 3;
}

message SearchRequest {
  string query = 1;
  string number = 2;
  map<string, int32> config = 3;
  repeated string a = 4;
  Type type = 5;
}

Mensagens Aninhadas

protobuf
message Outer {                  // Nível 0
  message MiddleAA {  // Nível 1
    message Inner {   // Nível 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // Nível 1
    message Inner {   // Nível 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

message pode aninhar declaração message dentro, é como aninhar structs.

Package

Pode-se adicionar um modificador de pacote opcional ao arquivo protobuf para prevenir conflito de nomes entre tipos de mensagens de protocolo.

protobuf
package foo.bar;
message Open { ... }

Então, pode-se usar o nome do pacote ao definir campos de tipos de mensagem:

protobuf
message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

Import

Importar permite que múltiplos arquivos protobuf compartilhem definições, sua sintaxe é assim, não pode omitir a extensão do arquivo ao importar.

protobuf
import "a/b/c.proto";

Ao importar, todos usam caminho relativo, este caminho relativo não se refere ao caminho relativo entre o arquivo importador e o arquivo importado, mas depende do caminho de escaneamento especificado quando o compilador protoc gera código, suponha que haja a seguinte estrutura de arquivo

pb_learn
│  common.proto

├─monster
│      monster.proto

└─player
        health.proto
        player.proto

Se precisarmos gerar apenas código da parte do diretório player, e ao escanear o caminho especificamos apenas o diretório player, então a importação mútua entre health.proto e player.proto pode escrever apenas o nome do arquivo único, por exemplo player.proto importa health.proto.

protobuf
import "health.proto";

Se neste momento player.proto importar common.proto ou arquivos do diretório monster, então falhará na compilação, então a escrita abaixo está completamente errada, porque o compilador não consegue encontrar estes arquivos.

go
import "../common.proto"; // Escrita errada

TIP

Aliás, .., . estes símbolos não são permitidos no caminho de importação.

Suponha que ao compilar especifique pb_learn como caminho de escaneamento, então pode-se importar arquivos de outros diretórios através de caminho relativo, o caminho real importado é o endereço absoluto deste arquivo relativo a pb_learn, veja abaixo o exemplo de player.proto importando outros arquivos.

protobuf
import "common.proto";
imrpot "monster/monster.proto";
import "player/health.proto";

Mesmo health.proto que está no mesmo diretório agora também deve usar caminho relativo. Então em um projeto, geralmente criamos uma pasta separada para armazenar todos os arquivos protobuf, e especificamos como caminho de escaneamento ao compilar, e todos os comportamentos de importação sob este diretório também são baseados em seu caminho relativo.

TIP

Se usar o editor goland, para seu diretório protobuf criado por você, por padrão não pode ser analisado, aparecerá vermelho, se quiser que o goland identifique, precisa definir manualmente o caminho de escaneamento, o princípio é exatamente o mesmo que explicado acima, o método de configuração é o seguinte, abra as seguintes configurações

File | Settings | Languages & Frameworks | Protocol Buffers

Adicione manualmente o caminho de escaneamento em Import Paths, este caminho de escaneamento deve ser consistente com o caminho especificado ao compilar.

Any

O tipo Any permite usar mensagens como tipos incorporados sem precisar de suas definições proto, podemos importar diretamente os tipos definidos pelo Google, é自带的, não precisa escrever manualmente.

protobuf
import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

O Google também pré-definiu muitos outros tipos, vá para protobuf/ptypes at master · golang/protobuf (github.com) para ver mais, principalmente incluindo

  • Encapsulamento de tipos básicos
  • Tipo de tempo
  • Tipo Duration

As definições protobuf deles devem estar no diretório inlucde do compilador protoc.

OneOf

A documentação oficial aqui dá uma explicação muito tediosa, falando de forma simples, na verdade representa que um campo terá múltiplos tipos possíveis durante a transmissão, mas finalmente apenas um tipo será usado, não é permitido aparecer campos modificados por repeated dentro dele, é como union em linguagem C.

protobuf
message Stock {
    // Dados específicos de Stock
}

message Currency {
    // Dados específicos de Currency
}

message ChangeNotification {
  int32 id = 1;
  oneof instrument {
    Stock stock = 2;
    Currency currency = 3;
  }
}

Service

A palavra-chave service pode definir um serviço RPC, um serviço RPC contém várias interfaces rpc, interfaces são divididas em interfaces unárias e interfaces de fluxo.

protobuf
message Body {
  string name = 1;
}

service ExampleService {
  rpc DoSomething(Body) returns(Body);
}

E interfaces de fluxo são divididas em fluxo unidirecional e fluxo bidirecional, geralmente usa a palavra-chave stream para modificar, veja um exemplo abaixo.

protobuf
message Body {
  string name = 1;
}

service ExampleService {
  // Fluxo do lado do cliente
  rpc DoSomething(stream Body) returns(Body);
  // Fluxo do lado do servidor
  rpc DoSomething1(Body) returns(stream Body);
  // Fluxo bidirecional
  rpc DoSomething2(stream Body) returns(stream Body);
}

O chamado fluxo é enviar dados mutuamente por longo tempo em uma conexão, não mais como a simples pergunta e resposta da interface unária.

Empty

empty na verdade é uma message vazia, corresponde a struct vazia em go, raramente é usada para modificar campos, principalmente é usada para indicar que alguma interface rpc não precisa de parâmetros ou não tem valor de retorno.

protobuf
syntax = "proto3";

import "google/protobuf/empty.proto";

service EmptyService {
  rpc Do(google.protobuf.Empty) returns(google.protobuf.Empty);
}

Option

option é geralmente usado para controlar alguns comportamentos do protobuf. Por exemplo, controlar o pacote gerado pelo código fonte da linguagem go, pode ser declarado como segue.

protobuf
option go_package = "github/jack/sample/pb_learn;pb_learn"

Antes do ponto e vírgula está o caminho de importação de outros arquivos de código após a geração do código, depois do ponto e vírgula é o nome do pacote do arquivo gerado correspondente.

Pode fazer algumas otimizações, tem os seguintes valores disponíveis, não pode declarar repetidamente.

  • SPEED, maior grau de otimização, maior volume de código gerado, por padrão é este.
  • CODE_SIZE, reduzirá o volume de geração de código, mas dependerá de reflexão para serialização
  • LIFE_RUNEIMTE, menor volume de código, mas faltará algumas características.

Abaixo está um caso de uso

protobuf
option optimize_for = SPEED;

Além disso, option também pode adicionar algumas metainformações a message e enum, usando reflexão pode-se obter estas informações, isso é particularmente útil ao fazer validação de parâmetros.

Compilação

Compilação é geração de código, acima apenas definiu o arquivo protobuf, ao usar na prática precisa convertê-lo em código fonte de alguma linguagem específica para usar, completamos isso através do compilador protoc, ele suporta múltiplas linguagens.

Instalação

Para baixar o compilador, vá para protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format (github.com) para baixar a versão Release mais recente, geralmente é um arquivo compactado

protoc-25.1-win64
│  readme.txt

├─bin
│      protoc.exe

└─include
    └─google
        └─protobuf
            │  any.proto
            │  api.proto
            │  descriptor.proto
            │  duration.proto
            │  empty.proto
            │  field_mask.proto
            │  source_context.proto
            │  struct.proto
            │  timestamp.proto
            │  type.proto
            │  wrappers.proto

            └─compiler
                    plugin.proto

Após baixar, adicione o diretório bin ao PATH para poder usar o comando protoc, após concluir, verifique a versão, se puder outputar normalmente significa que a instalação foi bem-sucedida

bash
$ protoc --version
libprotoc 25.1

O compilador baixado por padrão não suporta linguagem go, porque a geração de código da linguagem go é um arquivo executável separado, outras linguagens estão todas juntas, então instale o plugin de linguagem go, usado para traduzir definições protocbuf para código fonte da linguagem go.

bash
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

Se também precisar gerar código de serviço gRPC, instale o seguinte plugin

bash
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Após a instalação, verifique sua versão

bash
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.3.0

$ protoc-gen-go --version
protoc-gen-go.exe v1.31.0

Estes plugins também são arquivos binários separados, mas só podem ser chamados através de protoc, não podem ser executados sozinhos.

(this program should be run by protoc, not directly)

Além disso, há muitos outros plugins, como plugins para gerar documentação de interface openapi, etc, se interessado pode pesquisar por conta própria.

Geração

Ainda pegue o exemplo anterior, a estrutura é a seguinte

pb_learn
│  common.proto

├─monster
│      monster.proto

└─player
        health.proto
        player.proto

Para geração de código, no total deve especificar três parâmetros

  1. Caminho de escaneamento, indica ao compilador onde procurar arquivos protobuf e como analisar caminhos de importação
  2. Caminho de geração, onde colocar o arquivo após compilar
  3. Arquivo de destino, especificar quais arquivos de destino devem ser compilados.

Antes de começar, certifique-se de que go_package no arquivo protobuf está configurado corretamente, use protoc -h para verificar seus parâmetros suportados, o mais comum é -I ou --proto_path, pode ser usado múltiplas vezes para especificar múltiplos caminhos de escaneamento, por exemplo

bash
$ protoc --proto_path=./pb_learn --proto_path=./third_party

Apenas especificar o caminho de escaneamento não é suficiente, também precisa especificar o caminho de geração e o arquivo protobuf de destino, aqui é gerar arquivo go então usa o parâmetro --go_out, suportado pelo plugin protoc-gen-go baixado anteriormente.

bash
$ cd pb_learn

$ protoc --proto_path=. --go_out=. common.proto

$ ls
common.pb.go  common.proto  monster/  player/

O parâmetro --go_out especifica o caminho de geração, . significa o caminho atual, common.proto é o arquivo especificado para compilar. Se quiser gerar código grpc (premissa é que instalou o plugin grpc), pode adicionar o parâmetro --go-grpc_out (se não houver definição service no arquivo protobuf, não gerará o arquivo correspondente).

bash
$ protoc --proto_path=. --go_out=. --go-grpc_out=. common.proto

$ ls
common.pb.go  common.proto  common_grpc.pb.go  monster/  player/

common.pb.go é a definição de tipo protobuf gerada, common_grpc.pb.go é o código gRPC gerado, é baseado no anterior, se não gerar a definição da linguagem correspondente, também não poderá gerar código gRPC.

Se quiser compilar todos os arquivos protobuf sob este diretório, pode usar o curinga *, por exemplo ``

bash
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./*.proto

Se quiser incluir todos os arquivos, pode usar o curinga **, por exemplo ./**/*.proto.

bash
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./*.proto

Porém, este método só se aplica a shells que suportam este tipo de curinga, por exemplo, no windows, cmd ou powershell não suportam esta escrita

powershell
D> protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./**/*.proto
Invalid file name pattern or missing input file "./**/*.proto"

Felizmente, gitbash suporta muitos comandos linux, também pode fazer o windows suportar esta sintaxe. Para evitar ter que escrever comandos repetidos toda vez, pode colocá-los dentro de makefile

makefile
.PHONY: all

proto_gen:
  protoc --proto_path=. \
       --go_out=paths=source_relative:. \
       --go-grpc_out=paths=source_relative:. \
       ./**/*.proto ./*.proto

Pode-se notar que há um paths=source_relative:. adicional, isto é definir o modo de caminho de geração do arquivo, no total há as seguintes opções

  • paths=import, por padrão é este, o arquivo será gerado no diretório especificado por import, também pode ser um caminho de módulo. Por exemplo, agora há um arquivo protos/buzz.proto, especificando paths=example.com/project/protos/fizz, então finalmente gerará example.com/project/protos/fizz/buzz.pb.go.
  • module=$PREFIX, ao gerar, removerá o prefixo do caminho. No exemplo acima, se especificar o prefixo example.com/project, então finalmente gerará protos/fizz/buzz.pb.go, este modo é principalmente usado para gerar diretamente no módulo (parece que não há muita diferença).
  • paths=source_relative, o arquivo gerado manterá a mesma estrutura relativa que o arquivo protobuf no diretório especificado.

Após os dois pontos : é o caminho de geração especificado.

|  common.proto
|  common.pb.go

├─monster
│      monster.pb.go
│      monster.proto

└─player
        health.pb.go
        health.proto
        health_grpc.pb.go
        player.pb.go
        player.proto

Reflexão

Através de options pode-se estender enum e message, primeiro importe "google/protobuf/descriptor.proto"

protobuf
import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  optional string string_name = 123456789;
}

enum Integer {
  INT64 = 0[
    (string_name) = "int_64"
  ];
}

Isto equivale a adicionar uma metainformação a este valor enum. Para message também é o mesmo, como segue

protobuf
import "google/protobuf/descriptor.proto";

extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}

message MyMessage {
  option (my_option) = "Hello world!";
}

Isto equivale a ter reflexão sobre protobuf, após gerar o código pode-se acessar através de Descriptor, como segue

go
func main() {
  message := pb_learn.MyMessage{}
  message.ProtoReflect().Descriptor().Options().ProtoReflect().Range(func(descriptor protoreflect.FieldDescriptor, value protoreflect.Value) bool {
    fmt.Println(descriptor.FullName(), ":", value)
    return true
  })
}

Saída

my_option:"Hello world!"

Este método pode ser comparado a adicionar tag a struct em go, é quase a mesma sensação, de acordo com este método também pode implementar funcionalidade de validação de parâmetros, apenas escreva as regras em options, e use Descriptor para verificar.

Golang por www.golangdev.cn edit