Protobuf
Sitio web oficial: Protocol Buffers | Google Developers
Introducción
Tutorial oficial: Protocol Buffer Basics: Go | Protocol Buffers | Google Developers
Protocol Buffers es un mecanismo de serialización de datos estructurados extensible, independiente del lenguaje y del protocolo, abierto por Google en 2008. Es más rápido al desempacar y empaquetar, y se usa principalmente en la comunicación relacionada con RPC. Puede definir la forma estructurada de los datos, y luego puede usar código fuente generado especialmente para escribir y leer fácilmente datos estructurados desde y hacia varios flujos de datos, y usarlo en varios lenguajes. A continuación, Protocol Buffers se denominará uniformemente protobuf.
protobuf es bastante popular, especialmente en el ámbito de Go. gRPC lo utiliza como mecanismo de serialización para la transmisión de protocolos.
Sintaxis
Primero, veamos un ejemplo para ver cómo es aproximadamente un archivo protobuf. En general, su sintaxis es muy simple y puede dominarla en diez minutos. A continuación se muestra un ejemplo de un archivo llamado search.proto. La extensión de archivo de protobuf es .proto:
syntax = "proto3";
message SearchRequest {
string query = 1;
string number = 2;
}
message SearchResult {
string data = 1;
}
service SearchService {
rpc Search(SearchRequest) returns(SearchResult);
}- La primera línea
syntax = "proto3";indica que se usa la sintaxisproto3. Por defecto se usa la sintaxisproto3. messagese declara de manera similar a una estructura y es la estructura básica enproto.SearchRequestdefine tres campos. Cada campo tendrá un nombre y un tipo.servicedefine un servicio. Un servicio contiene uno o más interfaces rpc.- La interfaz rpc debe tener exactamente un parámetro y un valor de retorno. Sus tipos deben ser
message, no pueden ser tipos básicos.
Además, tenga en cuenta que cada línea en un archivo proto debe terminar con un punto y coma.
Comentarios
El estilo de comentarios es exactamente el mismo que en Go.
syntax = "proto3";
/* Comentario
* Comentario */
message SearchRequest {
string query = 1; //Comentario
string number = 2;
}Tipos
Los modificadores de tipo solo pueden aparecer en message, no pueden aparecer solos.
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
Agregue el modificador repeated antes de un tipo básico para indicar que es un tipo de array, correspondiente al slice en Go.
message Company {
repeated string employee = 1;
}map
El formato para definir un tipo map en protobuf es el siguiente:
map<key_type, value_type> map_field = N;key_type debe ser numérico o cadena. value_type no tiene restricción de tipo. Vea un ejemplo:
message Person {
map<string, int64> cards = 1;
}Campos
De hecho, proto no es un tipo tradicional de clave-valor. En el archivo proto declarado no aparecerán datos específicos. Después de cada = de campo debe seguir el número único actual en el message. Estos números se utilizan para identificar y definir estos campos en el cuerpo del mensaje binario. Los números comienzan desde 1. Los números del 1 al 15 ocupan 1 byte, y los del 16 al 2047 ocupan dos bytes. Por lo tanto, asigne los números del 1 al 15 a los campos que aparecen con frecuencia para ahorrar espacio, y debe dejar algo de espacio para los campos que puedan aparecer con frecuencia en el futuro.
Los campos en un message deben seguir las siguientes reglas:
singular: por defecto es un campo de ese tipo. En unmessagebien estructurado, solo puede haber 0 o 1 campo de este tipo, es decir, no puede haber el mismo campo repetido. La siguiente declaración causará un error:protobufsyntax = "proto3"; message SearchRequest { string query = 1; string number = 2; string number = 3;//Campo repetido }optional: similar asingular, solo que puede verificar explícitamente si se estableció el valor del campo. Puede haber las siguientes dos situaciones:set: se serializaráunset: no se serializará
repeated: este tipo de campo puede aparecer 0 o más veces. Los valores duplicados se conservarán en orden (en otras palabras, es un array, puede permitir que el mismo tipo de valor aparezca varias veces y se conserve en el orden en que aparece, es un índice).map: campo de tipo clave-valor. La forma de declaración es la siguiente:protobufmap<string,int32> config = 3;
Campos reservados
La palabra clave reserve puede declarar campos reservados. Después de declarar el número de campo reservado, ya no se puede usar como número y nombre de otros campos, y también ocurrirá un error durante la compilación. La respuesta oficial de Google es: si un archivo proto elimina algunos números en una nueva versión, entonces en el futuro otros usuarios pueden reutilizar estos números eliminados. Pero si vuelve a la versión anterior del número, causará un error porque el número correspondiente al campo es inconsistente. Los campos reservados pueden jugar un papel de recordatorio en el período de compilación, recordándole que no puede usar este campo reservado, de lo contrario la compilación no pasará.
syntax = "proto3";
message SearchRequest {
string query = 1;
string number = 2;
map<string, int32> config = 3;
repeated string a = 4;
reserved "a"; //Declarar campo con nombre específico como campo reservado
reserved 1 to 2; //Declarar una secuencia de números como campo reservado
reserved 3,4; //Declarar
}De esta manera, este archivo no pasará la compilación.
Campos obsoletos
Si un campo está obsoleto, se puede escribir de la siguiente manera:
message Body {
string name = 1 [deprecated = true];
}Enumeraciones
Puede declarar constantes de enumeración y usarlas como tipo de campo. Tenga en cuenta que el primer elemento del elemento de enumeración debe ser cero, porque el valor predeterminado del elemento de enumeración es el primer 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;
}Cuando hay elementos de enumeración con el mismo valor dentro del elemento de enumeración, se puede usar un alias de enumeración:
syntax = "proto3";
enum Type {
option allow_alias = true; //Necesita habilitar la opción de configuración que permite usar alias
GET = 0;
GET_ALIAS = 0; //Alias del elemento de enumeración 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;
}Mensajes anidados
message Outer { // Nivel 0
message MiddleAA { // Nivel 1
message Inner { // Nivel 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Nivel 1
message Inner { // Nivel 2
int32 ival = 1;
bool booly = 2;
}
}
}message puede anidar la declaración de message, como anidar estructuras.
Package
Puede agregar un modificador de paquete opcional a un archivo protobuf para evitar conflictos de nombres entre tipos de mensajes de protocolo.
package foo.bar;
message Open { ... }Luego, puede usar el nombre del paquete al definir campos en el tipo de mensaje:
message Foo {
...
foo.bar.Open open = 1;
...
}Import
La importación permite que múltiples archivos protobuf compartan definiciones. Su sintaxis es la siguiente. No se puede omitir la extensión del archivo al importar.
import "a/b/c.proto";Al importar, se usan rutas relativas. Esta ruta relativa no se refiere a la ruta relativa entre el archivo importado y el archivo importado, sino que depende de la ruta de escaneo especificada cuando el compilador protoc genera código. Suponga que tiene la siguiente estructura de archivo:
pb_learn
│ common.proto
│
├─monster
│ monster.proto
│
└─player
health.proto
player.protoSi solo necesitamos generar código para la parte del directorio player, y solo especificamos el directorio player al escanear la ruta, entonces las importaciones mutuas entre health.proto y player.proto pueden escribir directamente el nombre de archivo único. Por ejemplo, player.proto importa health.proto:
import "health.proto";Si en este momento player.proto importa common.proto o archivos en el directorio monster, la compilación fallará. Por lo tanto, la siguiente escritura es completamente incorrecta, porque el compilador no puede encontrar estos archivos:
import "../common.proto"; // Escritura incorrectaTIP
Dicho sea de paso, los símbolos .. y . no están permitidos en la ruta de importación.
Suponga que especifica pb_learn como ruta de escaneo durante la compilación. Entonces puede importar archivos de otros directorios a través de rutas relativas. La ruta de importación real es la dirección relativa de la dirección absoluta del archivo relativa a pb_learn. Vea el siguiente ejemplo de player.proto importando otros archivos:
import "common.proto";
imrpot "monster/monster.proto";
import "player/health.proto";Incluso health.proto en el mismo directorio debe usar una ruta relativa en este momento. Por lo tanto, en un proyecto, generalmente creamos una carpeta separada para almacenar todos los archivos protobuf y la especificamos como ruta de escaneo durante la compilación. Y todos los comportamientos de importación en este directorio también se basan en su ruta relativa.
TIP
Si usa el editor Goland, para su directorio protobuf creado por usted mismo, no se puede analizar de forma predeterminada, lo que resultará en un error. Si quiere que Goland lo reconozca, debe establecer manualmente la ruta de escaneo. El principio es exactamente el mismo que se describió anteriormente. El método de configuración es el siguiente: abra la siguiente configuración:
File | Settings | Languages & Frameworks | Protocol BuffersAgregue manualmente la ruta de escaneo en Import Paths. Esta ruta de escaneo debe ser consistente con la ruta especificada durante la compilación.

Any
El tipo Any le permite usar mensajes como tipos incrustados sin necesidad de sus definiciones proto. Podemos importar directamente los tipos definidos por Google. Viene incorporado y no necesita escribirlo manualmente.
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}Google también ha predefinido muchos otros tipos. Visite protobuf/ptypes at master · golang/protobuf (github.com) para ver más. Principalmente incluyen:
- Encapsulamiento de tipos básicos
- Tipo de tiempo
- Tipo Duration
Las definiciones protobuf de ellos deben estar en el directorio include del compilador protoc.
OneOf
La explicación dada por la documentación oficial aquí es demasiado engorrosa. En términos simples, significa que un campo puede tener múltiples tipos posibles durante la transmisión, pero finalmente solo se puede usar un tipo. No se permiten campos modificados por repeated dentro de él. Es como el union en el lenguaje C.
message Stock {
// Datos específicos de Stock
}
message Currency {
// Datos específicos de Currency
}
message ChangeNotification {
int32 id = 1;
oneof instrument {
Stock stock = 2;
Currency currency = 3;
}
}Service
La palabra clave service puede definir un servicio RPC. Un servicio RPC contiene varias interfaces rpc. Las interfaces se dividen en interfaces unarias e interfaces de flujo.
message Body {
string name = 1;
}
service ExampleService {
rpc DoSomething(Body) returns(Body);
}Las interfaces de flujo se dividen en flujo unidireccional y flujo bidireccional. Generalmente se modifican con la palabra clave stream. Vea el siguiente ejemplo:
message Body {
string name = 1;
}
service ExampleService {
// Flujo del lado del cliente
rpc DoSomething(stream Body) returns(Body);
// Flujo del lado del servidor
rpc DoSomething1(Body) returns(stream Body);
// Flujo bidireccional
rpc DoSomething2(stream Body) returns(stream Body);
}El llamado flujo es enviar datos mutuamente a largo plazo en una conexión, ya no es una simple pregunta y respuesta como en la interfaz unaria.
Empty
empty es en realidad un message vacío, correspondiente a una estructura vacía en Go. Rara vez se usa para modificar campos. Se usa principalmente para indicar que una interfaz rpc no requiere parámetros o no tiene valor de retorno.
syntax = "proto3";
import "google/protobuf/empty.proto";
service EmptyService {
rpc Do(google.protobuf.Empty) returns(google.protobuf.Empty);
}Option
option se usa generalmente para controlar algunos comportamientos de protobuf. Por ejemplo, para controlar el paquete generado por el código fuente del lenguaje Go, se puede declarar de la siguiente manera:
option go_package = "github/jack/sample/pb_learn;pb_learn"Lo que está antes del punto y coma es la ruta de importación de otros archivos fuente después de que se genera el código, y lo que está después del punto y coma es el nombre del paquete del archivo generado correspondiente.
Puede hacer algunas optimizaciones. Hay los siguientes valores disponibles y no se pueden declarar repetidamente:
SPEED: el grado de optimización más alto, el volumen de código generado es el más grande. Este es el predeterminado.CODE_SIZE: reducirá el volumen de generación de código, pero dependerá de la reflexión para la serialización.LITE_RUNTIME: el volumen de código es el más pequeño, pero faltan algunas características.
A continuación se muestra un caso de uso:
option optimize_for = SPEED;Además, option también puede agregar algo de metainformación a message y enum. Esta información se puede obtener mediante reflexión, lo cual es particularmente útil al realizar la validación de parámetros.
Compilación
La compilación es la generación de código. Arriba solo se definió el archivo protobuf. Cuando se usa realmente, debe convertirse en código fuente de un lenguaje específico para usarlo. Completamos esto a través del compilador protoc, que admite múltiples lenguajes.

Instalación
Para descargar el compilador, vaya a protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format (github.com) para descargar la última versión de Release. Generalmente es un archivo comprimido:
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.protoDespués de descargar, agregue el directorio bin al PATH para poder usar el comando protoc. Después de completar, verifique la versión. Si puede salir normalmente, significa que la instalación fue exitosa:
$ protoc --version
libprotoc 25.1El compilador descargado no admite el lenguaje Go de forma predeterminada, porque la generación de código del lenguaje Go es un archivo ejecutable separado, y otros lenguajes están todos juntos. Entonces instale el complemento del lenguaje Go para traducir la definición protobuf al código fuente del lenguaje Go.
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latestSi también necesita generar código de servicio gRPC, instale el siguiente complemento:
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestDespués de la instalación, verifique su versión:
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.3.0
$ protoc-gen-go --version
protoc-gen-go.exe v1.31.0Estos complementos también son archivos binarios independientes, pero solo se pueden llamar a través de protoc y no se pueden ejecutar por separado.
(this program should be run by protoc, not directly)Además, hay muchos otros complementos, como complementos para generar documentación de interfaz openapi, etc. Si está interesado, puede buscarlos usted mismo.
Generación
Todavía tomemos el ejemplo anterior. La estructura es la siguiente:
pb_learn
│ common.proto
│
├─monster
│ monster.proto
│
└─player
health.proto
player.protoPara generar código, se deben especificar un total de tres parámetros:
- Ruta de escaneo: indica al compilador dónde buscar archivos
protobufy cómo analizar rutas de importación. - Ruta de generación: dónde se colocan los archivos compilados.
- Archivo objetivo: especifica qué archivos objetivo deben compilarse.
Antes de comenzar, asegúrese de que go_package en el archivo protobuf esté configurado correctamente. Use protoc -h para ver los parámetros admitidos. Los más comunes son -I o --proto_path, que se pueden usar varias veces para especificar múltiples rutas de escaneo. Por ejemplo:
$ protoc --proto_path=./pb_learn --proto_path=./third_partySolo especificar la ruta de escaneo no es suficiente. También debe especificar la ruta de generación y el archivo protobuf objetivo. Aquí se generan archivos go, por lo que se usa el parámetro --go_out, soportado por el complemento protoc-gen-go descargado anteriormente:
$ cd pb_learn
$ protoc --proto_path=. --go_out=. common.proto
$ ls
common.pb.go common.proto monster/ player/El parámetro --go_out especifica la ruta de generación. . significa la ruta actual. common.proto es el archivo a compilar. Si desea generar código grpc (siempre que tenga el complemento grpc instalado), puede agregar el parámetro --go-grpc_out (si no hay definición de service en el archivo protobuf, no se generará el archivo correspondiente):
$ 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 es la definición de tipo protobuf generada. common_grpc.pb.go es el código gRPC generado. Se basa en el primero. Si no hay definición de lenguaje correspondiente, no se puede generar código gRPC.
Si desea compilar todos los archivos protobuf en el directorio, puede usar el comodín *, como *.proto:
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./*.protoSi desea incluir todos los archivos, puede usar el comodín **, como ./**/*.proto:
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./**/*.protoSin embargo, este método solo es aplicable a shells que admiten este tipo de comodines. Por ejemplo, en Windows, ni cmd ni powershell admiten esta escritura:
D> protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./**/*.proto
Invalid file name pattern or missing input file "./**/*.proto"Afortunadamente, gitbash admite muchos comandos de Linux y también puede hacer que Windows admita esta sintaxis. Para evitar tener que escribir comandos repetidos cada vez, puede colocarlos en un makefile:
.PHONY: all
proto_gen:
protoc --proto_path=. \
--go_out=paths=source_relative:. \
--go-grpc_out=paths=source_relative:. \
./**/*.proto ./*.protoSe puede notar que se agrega paths=source_relative:.. Esto establece el modo de ruta de generación de archivos. Hay las siguientes opciones disponibles:
paths=import: este es el predeterminado. El archivo se generará en el directorio especificado porimport. También puede ser una ruta de módulo. Por ejemplo, si hay un archivoprotos/buzz.protoy se especificapaths=example.com/project/protos/fizz, finalmente se generaráexample.com/project/protos/fizz/buzz.pb.go.module=$PREFIX: al generar, se eliminará el prefijo de ruta. En el ejemplo anterior, si se especifica el prefijoexample.com/project, finalmente se generaráprotos/fizz/buzz.pb.go. Este modo se usa principalmente para generarlo directamente en el módulo (parece que no hay mucha diferencia).paths=source_relative: el archivo generado mantendrá la misma estructura relativa que el archivoprotobufen el directorio especificado.
Después del dos puntos : está la ruta de generación especificada.
| 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.protoReflexión
Puede extender enum y message a través de options. Primero 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"
];
}Esto equivale a agregar metainformación al valor de enumeración. Lo mismo es válido para message:
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}Esto equivale a tener reflexión sobre protobuf. Después de generar el código, se puede acceder a través de Descriptor:
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
})
}Salida:
my_option:"Hello world!"Esta forma se puede comparar con agregar tag a una estructura en Go. Es una sensación similar. Según esta forma, también se puede implementar la función de validación de parámetros. Solo necesita escribir reglas en options y verificar a través de Descriptor.
