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.
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 sintaxeproto3, por padrão usa a sintaxeproto3. messageé declarado de forma similar a uma struct, é a estrutura básica emprotoSearchRequestdefine três campos, cada campo terá nome e tiposervicedefine 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.
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 Proto | Tipo Go |
|---|---|
| double | float64 |
| float | float32 |
| int32 | int32 |
| int64 | int64 |
| uint32 | uint32 |
| uint64 | uint64 |
| sint32 | int32 |
| sint64 | int64 |
| fixed32 | uint32 |
| fixed64 | uint64 |
| sfixed32 | int32 |
| sfixed64 | int64 |
| bool | bool |
| string | string |
| 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.
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
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 umamessagebem 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.protobufsyntax = "proto3"; message SearchRequest { string query = 1; string number = 2; string number = 3;//Campo repetido }optional: Similar asingular, apenas pode verificar explicitamente se o valor do campo foi definido, pode haver as seguintes duas situaçõesset: Será serializadounset: 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 segueprotobufmap<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á.
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.
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.
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
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
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.
package foo.bar;
message Open { ... }Então, pode-se usar o nome do pacote ao definir campos de tipos de mensagem:
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.
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.protoSe 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.
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.
import "../common.proto"; // Escrita erradaTIP
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.
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 BuffersAdicione 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.
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.
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.
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.
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.
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.
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çãoLIFE_RUNEIMTE, menor volume de código, mas faltará algumas características.
Abaixo está um caso de uso
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.protoApó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
$ protoc --version
libprotoc 25.1O 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.
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latestSe também precisar gerar código de serviço gRPC, instale o seguinte plugin
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestApós a instalação, verifique sua versão
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.3.0
$ protoc-gen-go --version
protoc-gen-go.exe v1.31.0Estes 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.protoPara geração de código, no total deve especificar três parâmetros
- Caminho de escaneamento, indica ao compilador onde procurar arquivos
protobufe como analisar caminhos de importação - Caminho de geração, onde colocar o arquivo após compilar
- 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
$ protoc --proto_path=./pb_learn --proto_path=./third_partyApenas 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.
$ 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).
$ 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 ``
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./*.protoSe quiser incluir todos os arquivos, pode usar o curinga **, por exemplo ./**/*.proto.
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./*.protoPoré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
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
.PHONY: all
proto_gen:
protoc --proto_path=. \
--go_out=paths=source_relative:. \
--go-grpc_out=paths=source_relative:. \
./**/*.proto ./*.protoPode-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 porimport, também pode ser um caminho de módulo. Por exemplo, agora há um arquivoprotos/buzz.proto, especificandopaths=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 prefixoexample.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 arquivoprotobufno 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.protoReflexão
Através de options pode-se estender enum e message, primeiro importe "google/protobuf/descriptor.proto"
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
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
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.
