2024年Go最全高效数据编码-protobuf_protocol buffer在线编码,2024年最新成功收获美团,2024年最新小米offer

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

每个消息定义中的每个字段都有唯一的编号。这些字段编号用于标识消息二进制格式中的字段,并且在使用消息类型后不应更改。请注意,范围 1 到 15 中的字段编号需要一个字节进行编码,包括字段编号和字段类型。范围 16 至 2047 中的字段编号需要两个字节。所以你应该保留数字 1 到 15 作为非常频繁出现的消息元素。请记住为将来可能添加的频繁出现的元素留出一些空间。
可以指定的最小字段编号为1,最大字段编号为229-1 或 536,870,911。也不能使用数字 19000 到 19999(FieldDescriptor :: kFirstReservedNumber 到 FieldDescriptor :: kLastReservedNumber),因为它们是为 Protocol Buffers实现保留的。
如果在 .proto 中使用这些保留数字中的一个,Protocol Buffers 编译的时候会报错。
同样,您不能使用任何以前 Protocol Buffers 保留的一些字段号码。

2.1.2 指定字段规则

消息字段可以是以下之一:

  • singular:格式正确的消息可以有零个或一个此字段(但不超过一个)。这是 proto3 语法的默认字段规则。
  • repeated:该字段可以在格式良好的消息中重复任意次数(包括零次)。重复值的顺序将被保留。

在 proto3 中,repeated标量数值类型的字段packed默认使用编码。

2.1.3 保留字段

如果您通过完全删除某个字段或将其注释掉来更新消息类型,那么未来的用户可以在对该类型进行自己的更新时重新使用该字段号。如果稍后加载到了的旧版本 .proto 文件,则会导致服务器出现严重问题,例如数据混乱,隐私错误等等。确保这种情况不会发生的一种方法是指定删除字段的字段编号(或名称,这也可能会导致 JSON 序列化问题)为 reserved。如果将来的任何用户试图使用这些字段标识符,Protocol Buffers 编译器将会报错。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

2.2 各个语言的标量类型对应关系

.proto type说明C++GoJavaPython
doubledoublefloat64doublefloat
floatfloatfloat32floatfloat
int32使用可变长度编码。编码负数效率低下 - 如果您的字段可能具有负值,请改用 sint32。int32int32intint
int64使用可变长度编码。编码负数效率低下 - 如果您的字段可能具有负值,请改用 sint64。int64int64longint/long
uint32使用可变长度编码。uint32uint32intint/long
uint64使用可变长度编码。uint64uint64longint/long
sint32使用可变长度编码。有符号整数值。这些比常规 int32 更有效地编码负数。int32int32intint
sint64使用可变长度编码。有符号整数值。这些比常规 int64 更有效地编码负数。int64int64longint/long
fixed32总是四个字节。如果值通常大于 2^28 ,则比 uint32 更有效。uint32uint32intint/long
fixed64总是八个字节。如果值通常大于 2 ^56 ,则比 uint64 更有效。uint64uint64longint/long
sfixed32总是四个字节。int32int32intint
sfixed64总是八个字节。int64int64longint/long
boolboolboolbooleanbool
string字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不能超过 2 ^ 32。stringstringStringstr/unicode
bytes可以包含不超过 2^ 32 的任意字节序列string[]byteByteStringstr(py2)/unicode(py3)
默认值

解析消息时,如果编码的消息不包含特定的单数元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值是特定于类型的:

  • 对于string,默认值为空字符串。
  • 对于bytes,默认值为空字节。
  • 对于bool,默认值为 false。
  • 对于数字类型,默认值为零。
  • 对于枚举类型,默认值是第一个定义的 enum value,它必须是 0。
  • 对于消息字段,未设置该字段。它的确切值取决于语言。

重复字段的默认值为空(通常是相应语言的空列表)。

枚举
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}

枚举类型需要注意的是,一定要有 0 值。

  • 枚举为 0 的是作为零值,当不赋值的时候,就会是零值。
  • 为了和 proto2 兼容。在 proto2 中,零值必须是第一个值。
  • 您可以通过为不同的枚举常量分配相同的值来定义别名。为此,您需要将该allow_alias选项设置为true,否则协议编译器将在找到别名时生成错误消息。

另外在反序列化的过程中,无法被识别的枚举值,将会被保留在 messaage 中。因为消息反序列化时如何表示是依赖于语言的。在支持指定符号范围之外的值的开放枚举类型的语言中,例如 C++ 和 Go,未知的枚举值只是存储为其基础整数表示。在诸如 Java 之类的封闭枚举类型的语言中,枚举值会被用来标识未识别的值,并且特殊的访问器可以访问到底层整数。
在其他情况下,如果消息被序列化,则无法识别的值仍将与消息一起序列化。

2.3 使用其他消息类型

您可以使用其他消息类型作为字段类型。例如,假设您想Result在每条SearchResponse消息中包含消息 - 为此,您可以在其中定义一个Result消息类型.proto,然后指定一个类型为Resultin的字段SearchResponse:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

2.3.1 导入其他proto

在上面的例子中,Result消息类型是在同一个文件中定义的SearchResponse——如果你想用作字段类型的消息类型已经在另一个.proto文件中定义了怎么办?
您可以.proto通过_导入_其他文件中的定义来使用它们。要导入 another.proto的定义,请在文件顶部添加一个 import 语句:

import "myproject/other_protos.proto";

2.4 嵌套类型

您可以在其他消息类型中定义和使用消息类型,如下例所示——这里的Result消息是在SearchResponse消息内部定义的:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

如果要在其父消息类型之外重用此消息类型,则将其称为_Parent_.Type

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

可以随意嵌套消息

message Outer {                  // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

2.5 更新消息

如果后面发现之前定义 message 需要增加字段了,这个时候就体现出 Protocol Buffer 的优势了,不需要改动之前的代码。不过需要满足以下 10 条规则:

  1. 不要改动原有字段的数据结构。
  2. 如果您添加新字段,则任何由代码使用“旧”消息格式序列化的消息仍然可以通过新生成的代码进行分析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息进行交互。同样,由新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时会简单地忽略新字段。
  3. 只要字段号在更新的消息类型中不再使用,字段可以被删除。您可能需要重命名该字段,可能会添加前缀“OBSOLETE_”,或者标记成保留字段号 reserved,以便将来的 .proto 用户不会意外重复使用该号码。
  4. int32,uint32,int64,uint64 和 bool 全都兼容。这意味着您可以将字段从这些类型之一更改为另一个字段而不破坏向前或向后兼容性。如果一个数字从不适合相应类型的线路中解析出来,则会得到与在 C++ 中将该数字转换为该类型相同的效果(例如,如果将 64 位数字读为 int32,它将被截断为 32 位)。
  5. sint32 和 sint64 相互兼容,但与其他整数类型不兼容。
  6. 只要字节是有效的UTF-8,string 和 bytes 是兼容的。
  7. 嵌入式 message 与 bytes 兼容,如果 bytes 包含 message 的 encoded version。
  8. fixed32与sfixed32兼容,而fixed64与sfixed64兼容。
  9. enum 就数组而言,是可以与 int32,uint32,int64 和 uint64 兼容(请注意,如果它们不适合,值将被截断)。但是请注意,当消息反序列化时,客户端代码可能会以不同的方式对待它们:例如,未识别的 proto3 枚举类型将保留在消息中,但消息反序列化时如何表示是与语言相关的。(这点和语言相关,上面提到过了)Int 域始终只保留它们的值。
  10. 将单个值更改为新的成员是安全和二进制兼容的。如果您确定一次没有代码设置多个字段,则将多个字段移至新的字段可能是安全的。将任何字段移到现有字段中都是不安全的。(注意字段和值的区别,字段是 field,值是 value)

2.6 未知字段

未知数字段是 protocol buffers 序列化的数据,表示解析器无法识别的字段。例如,当一个旧的二进制文件解析由新的二进制文件发送的新数据的数据时,这些新的字段将成为旧的二进制文件中的未知字段。
Proto3 实现可以成功解析未知字段的消息,但是,实现可能会或可能不会支持保留这些未知字段。你不应该依赖保存或删除未知域。对于大多数 Google protocol buffers 实现,未知字段在 proto3 中无法通过相应的 proto 运行时访问,并且在反序列化时被丢弃和遗忘。这是与 proto2 的不同行为,其中未知字段总是与消息一起保存并序列化。

2.7 Any

该Any消息类型,可以使用邮件作为嵌入式类型,而不必自己.proto定义。AnAny包含一个任意序列化的消息 as bytes,以及一个 URL,该 URL 充当全局唯一标识符并解析为该消息的类型。要使用该Any类型,您需要导入 google/protobuf/any.proto.

import "google/protobuf/any.proto";

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

给定消息类型的默认类型 URL 是type.googleapis.com/packagename.messagename

不同的语言实现将支持运行时库助手以类型安全的方式打包和解包 Any 值——例如,在 Java 中,Any 类型将具有特殊的pack()andunpack()访问器,而在 C++ 中有PackFrom()andUnpackTo()方法:

// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);

// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const Any& detail : status.details()) {
  if (detail.Is<NetworkErrorDetails>()) {
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error);
    ... processing network_error ...
  }
}

目前,用于处理 Any 类型的运行时库正在开发中

Once

如果您的消息包含多个字段并且最多同时设置一个字段,则可以强制执行此行为并使用 oneof 功能节省内存。
oneof字段和普通字段一样,除了oneof共享内存中的所有字段外,最多可以同时设置一个字段。设置 oneof 的任何成员会自动清除所有其他成员。您可以使用特殊case()或WhichOneof()方法检查 oneof 中设置的值(如果有),具体取决于您选择的语言。
要在您的 oneof 中定义 oneof,您.proto可以使用oneof后跟 oneof 名称的关键字,在这种情况下test_oneof:

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

  • 设置 oneof 字段将自动清除 oneof 的所有其他成员。因此,如果您设置了多个 oneof 字段,则只有您设置的_最后一个_字段仍然具有值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // Will clear name field.
CHECK(!message.has_name());

  • 如果解析器遇到同一 oneof 的多个成员,则在解析的消息中仅使用看到的最后一个成员。
  • oneof 不能是repeated。
  • 反射 API 适用于 oneof 字段。
  • 如果将 oneof 字段设置为默认值(例如将 int32 oneof 字段设置为 0),则将设置该 oneof 字段的“大小写”,并且该值将在线上序列化。
  • 如果您使用 C++,请确保您的代码不会导致内存崩溃。以下示例代码将崩溃,因为sub_message已通过调用该set_name()方法删除。
SampleMessage message;
SubMessage\* sub_message = message.mutable\_sub\_message();
message.set\_name("name");      // Will delete sub\_message
sub_message->set_...            // Crashes here

  • 同样在 C++ 中,如果你的Swap()两条消息带有 oneofs,则每条消息都会以另一个的 oneof 情况结束:在下面的示例中,msg1将有一个sub_message和msg2将有一个name.
SampleMessage msg1;
msg1.set\_name("name");
SampleMessage msg2;
msg2.mutable\_sub\_message();
msg1.swap(&msg2);
CHECK(msg1.has\_sub\_message());
CHECK(msg2.has\_name());

  • 向后兼容性: 添加或删除Once字段时要小心。如果检查 oneof 的值返回None/ NOT_SET,则可能意味着尚未设置 oneof 或已将其设置为不同版本的 oneof 中的字段。无法区分,因为无法知道未知字段是否是 oneof 的成员。

2.8 Map

如果你想创建一个关联映射作为数据定义的一部分,protocol buffers 提供了一个方便的快捷语法:

map<key_type, value_type> map_field = N;

  • key类型可以是任何整数或字符串类型。
  • enum不能作为key。
  • map字段不能是repeated
  • 线性数组和map迭代顺序是不确定的,所以你不能依靠你的map是在一个 特定顺序。
  • 为proto生成文本格式时,map按key排序,数字的key按数字排序
  • 从数组中解析或合并时,如果有重复的key,则使用所看到的最后一个key。从文本格式解析映射时,如果有重复的key,解析可能会失败。

可以自己实现maps

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}
repeated MapFieldEntry map_field = N;

2.9 Json映射

Proto3 支持 JSON 中的规范编码,使系统之间共享数据变得更加容易。编码在下表中按类型逐个描述。
如果 JSON 编码数据中缺少值或其值为空,则在解析为 protocol buffer 时,它将被解释为适当的默认值。如果一个字段在协议缓冲区中具有默认值,默认情况下它将在 JSON 编码数据中省略以节省空间。具体 Mapping 的实现可以提供选项决定是否在 JSON 编码的输出中发送具有默认值的字段。

protoJsonJson示例说明
messageobject{“fooBar”, v, “g”: null}生成 JSON 对象。消息字段名称映射到小写字母并成为 JSON 对象键。如果指定了字段选项,则指定的值将用作键。解析器接受lowerCamelCase 名称(或选项指定的名称)和原始原型字段名称。是所有字段类型的可接受值,并被视为相应字段类型的默认值。​
enumstring“FOO_BAR”使用 proto 中指定的枚举值的名称。解析器接受枚举名称和整数值。
mapobject{“k”: v,…}键值转化成json对象
repeated Varray{v, …}Json值将是json数组
stringstring“Hello”
bytesbase64string“YWJHASDFAFGQWRWR+”JSON 值将是使用带填充的标准 base64 编码编码为字符串的数据。接受带/不带填充的标准或 URL 安全 base64 编码。
int32fixed32uint32number1,-10,0JSON 值将是一个十进制数。接受数字或字符串。
int64fixed64uint64string“1”, “-10”JSON 值将是一个十进制字符串。接受数字或字符串。
float,doublenumber1.1, -10.0, “NaN”,“Infinity”JSON 值将是数字或特殊字符串值“NaN”、“Infinity”和“-Infinity”之一。接受数字或字符串。指数符号也被接受。-0 被认为等价于 0。
Anyobject{“@type”: “url”, “f”:v}如果 Any 包含一个具有特殊 JSON 映射的值,它将按如下方式转换:. 否则,该值将转换为 JSON 对象,并插入该字段以指示实际数据类型。{“@type”: xxx, “value”: yyy}“@type”
Timestampstring“1972-0101T10:00:20.021Z”使用 RFC 3339,其中生成的输出将始终是 Z 规范化的,并使用 0、3、6 或 9 个小数位。也接受除“Z”以外的偏移量。
Durationstring“1.000340012s”, “1s”生成的输出始终包含 0、3、6 或 9 个小数位,具体取决于所需的精度,后跟后缀“s”。接受任何小数位数(也没有),只要它们适合纳秒精度并且需要后缀“s”。
Structobjectjson对象
Wrapper typesvarious type2,“2”,“foo”,true包装器在 JSON 中使用与包装基元类型相同的表示,除了null在数据转换和传输期间允许和保留。
FileMaskstring“f.fooBar,h”
ListValuearray[foo, bar,…]
NullValuenull
Emptyobject{}

2.10 选项

选项不会改变声明的整体含义,但可能会影响它在特定上下文中的处理方式。可用选项的完整列表在 中定义google/protobuf/descriptor.proto
有些选项是文件级选项,这意味着它们应该写在顶级范围内,而不是在任何消息、枚举或服务定义中。一些选项是消息级别的选项,这意味着它们应该写在消息定义中。有些选项是字段级选项,这意味着它们应该写在字段定义中。选项也可以写在枚举类型、枚举值、字段之一、服务类型和服务方法上;

  • java_package(文件选项):要用于生成的 Java/Kotlin 类的包。如果文件中未java_package给出显式选项.proto,则默认情况下将使用 proto 包(使用文件中的“package”关键字指定.proto)。然而,proto 包通常不会成为好的 Java 包,因为 proto 包不会以反向域名开头。如果不生成 Java 或 Kotlin 代码,则此选项无效。
option java_package = "com.example.foo";

  • java_outer_classname(文件选项):要生成的包装 Java 类的类名(以及文件名)。如果文件中没有明确java_outer_classname指定.proto,类名将通过将.proto文件名转换为驼峰式大小写来构造(因此foo_bar.proto变为FooBar.java)。如果该java_multiple_files选项被禁用,则所有其他类/枚举/等。为.proto文件生成的将_在此外部_包装器 Java 类中生成为嵌套类/枚举/等。如果不生成 Java 代码,则此选项无效。
option java_outer_classname = "Ponycopter";

  • java_multiple_files(文件选项):如果为 false,则只.java为该.proto文件生成一个文件,所有 Java 类/枚举/等。为顶级消息、服务和枚举生成的消息将嵌套在外部类中(请参阅 参考资料java_outer_classname)。如果为 true,.java将为每个 Java 类/枚举/等生成单独的文件。为顶级消息、服务和枚举生成,并且为此.proto文件生成的包装器 Java 类将不包含任何嵌套类/枚举/等。这是一个布尔选项,默认为false。如果不生成 Java 代码,则此选项无效。
option java_multiple_files = true;

  • optimize_for(文件选项):可以设置为SPEED、CODE_SIZE、 或LITE_RUNTIME。这会通过以下方式影响 C++ 和 Java 代码生成器(以及可能的第三方生成器):
    • SPEED(默认):协议缓冲区编译器将生成用于序列化、解析和对您的消息类型执行其他常见操作的代码。这段代码是高度优化的。
    • CODE_SIZE:protocol buffer 编译器将生成最少的类,并将依赖共享的、基于反射的代码来实现序列化、解析和各种其他操作。因此生成的代码将比 with 小得多SPEED,但操作会更慢。类仍将实现与它们在SPEED模式中完全相同的公共 API 。此模式在包含大量.proto文件且不需要所有文件都非常快的应用程序中最有用。
    • LITE_RUNTIME:protocol buffer 编译器将生成仅依赖于“lite”运行时库(libprotobuf-lite而不是libprotobuf)的类。lite 运行时比完整库小得多(大约小一个数量级),但省略了某些功能,如描述符和反射。这对于在手机等受限平台上运行的应用程序特别有用。编译器仍然会像在SPEEDmode 中那样生成所有方法的快速实现。生成的类只会实现MessageLite每种语言的接口,它只提供完整Message接口的方法的一个子集。
option optimize_for = CODE_SIZE;

  • cc_enable_arenas(文件选项):为 C++ 生成的代码启用arena 分配
  • objc_class_prefix(文件选项):设置 Objective-C 类前缀,该前缀添加到所有 Objective-C 生成的类和此 .proto 中的枚举。没有默认值。您应该使用Apple 推荐的3-5 个大写字符之间的前缀。请注意,所有 2 个字母前缀都由 Apple 保留。
  • deprecated(字段选项):如果设置为true,则表示该字段已弃用,不应由新代码使用。在大多数语言中,这没有实际效果。在 Java 中,这变成了一个@Deprecated注解。将来,其他特定于语言的代码生成器可能会在字段的访问器上生成弃用注释,这反过来会导致在编译尝试使用该字段的代码时发出警告。如果该字段未被任何人使用并且您希望阻止新用户使用它,请考虑使用保留语句替换该字段声明。
int32 old_field = 6 [deprecated = true];

  • go_package: 可以指明生成的文件应该放在那个目录之下,可以指定包名。
    如何使用?
# 会在当前目录下创建目录hello/proto/v1,然后在v1下生成pb的go源文件,并且包名为v1
option go_package="hello/proto/v1";
# 会在当前目录的上级目录创建hello/proto/v1,然后在v1下生成pb的go源文件,并且包名为v1
option go_package="../hello/proto/v1";


2.11 命令说明

protoc --proto_path=IMPORT_PATH \
			--cpp_out=DST_DIR  \
      --java_out=DST_DIR \
      --python_out=DST_DIR \ 
      --go_out=DST_DIR \
      --ruby_out=DST_DIR \ 
      --objc_out=DST_DIR  \ 
      --csharp_out=DST_DIR  \
      path/to/file.proto 

  • IMPORT_PATH指定.proto解析import指令时查找文件的目录,如果省略则使用当前目录。通过–proto_path多次传递该选项可以指定多个导入目录,并按顺序搜索他们 -I=IMPORT_PATH可以当做–proto_path
  • 输出指定的代码
    • –cpp_out生成C++代码的输出目录
    • –java_out生成java代码的输出目录
    • –kotlin_out生成kotlin代码的输出目录
    • –python_out生成python代码的输出目录
    • –go_out生成go代码的输出目录
    • –ruby_out生成ruby的输出目录
    • –objc_out生成objectc代码的输出目录
    • –csharp_out生成的C#代码的输出目录
    • –php_out生成的php代码的输出目录
  • 您必须提供一个或多个.proto文件作为输入。.proto可以一次指定多个文件。尽管文件是相对于当前目录命名的,但每个文件都必须驻留在IMPORT_PATHs之一中,以便编译器可以确定其规范名称。
2.11.1 生成go的pb源文件
protoc -I . helloworld.proto  --go_out=plugins=grpc:.

三、proto3定义Services

如果要使用 RPC(远程过程调用)系统的消息类型,可以在 .proto 文件中定义 RPC 服务接口,protocol buffer 编译器将使用所选语言生成服务接口代码和 stubs。所以,例如,如果你定义一个 RPC 服务,入参是 SearchRequest 返回值是 SearchResponse,你可以在你的 .proto 文件中定义它,如下所示:

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

与 protocol buffer 一起使用的最直接的 RPC 系统是 gRPC:在谷歌开发的语言和平台中立的开源 RPC 系统。gRPC 在 protocol buffer 中工作得非常好,并且允许你通过使用特殊的 protocol buffer 编译插件,直接从 .proto 文件中生成 RPC 相关的代码。
如果你不想使用 gRPC,也可以在你自己的 RPC 实现中使用 protocol buffers。您可以在 Proto2 语言指南中找到更多关于这些相关的信息。
还有一些正在进行的第三方项目为 Protocol Buffers 开发 RPC 实现。

四、规范

4.1 标准文件格式

  • 保持行长度为 80 个字符。
  • 使用 2 个空格的缩进。
  • 最好对字符串使用双引号。

4.2 文件结构

文件应该命名 lower_snake_case.proto
所有文件应按以下方式排序:

  1. 许可证
  2. 文件简介
  3. Syntax
  4. Package
  5. Import
  6. File options
  7. Everythins else

message 采用驼峰命名法。message 首字母大写开头。字段名采用下划线分隔法命名。

message SongServerRequest {
  required string song_name = 1;
}

枚举类型采用驼峰命名法。枚举类型首字母大写开头。每个枚举值全部大写,并且采用下划线分隔法命名。

enum Foo {
  FIRST_VALUE = 0;
  SECOND_VALUE = 1;
}

每个枚举值用分号结束,不是逗号
服务名和方法名都采用驼峰命名法。并且首字母都大写开头。

service FooService {
  rpc GetSomething(FooRequest) returns (FooResponse);
}

五、编码原理

5.1 Base 128 Varints 编码

Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。
Varint 中的每个字节(最后一个字节除外)都设置了最高有效位(msb),这一位表示还会有更多字节出现。每个字节的低 7 位用于以 7 位组的形式存储数字的二进制补码表示,最低有效组首位。
如果用不到 1 个字节,那么最高有效位设为 0 ,如下面这个例子,1 用一个字节就可以表示,所以 msb 为 0.

0000 0001

如果需要多个字节表示,msb 就应该设置为 1 。例如 300,如果用 Varint 表示的话:

1010 1100 0000 0010

如果按照正常的二进制计算的话,这个表示的是 88068(65536 + 16384 + 4096 + 2048 + 4)。
那 Varint 是怎么编码的呢?
下面代码是 Varint int 32 的编码计算方法。

char\* EncodeVarint32(char\* dst, uint32\_t v) {
  // Operate on characters as unsigneds
  unsigned char\* ptr = reinterpret_cast<unsigned char\*>(dst);
  static const int B = 128;
  if (v < (1<<7)) {
    \*(ptr++) = v;
  } else if (v < (1<<14)) {
    \*(ptr++) = v | B;
    \*(ptr++) = v>>7;
  } else if (v < (1<<21)) {
    \*(ptr++) = v | B;
    \*(ptr++) = (v>>7) | B;
    \*(ptr++) = v>>14;
  } else if (v < (1<<28)) {
    \*(ptr++) = v | B;
    \*(ptr++) = (v>>7) | B;
    \*(ptr++) = (v>>14) | B;
    \*(ptr++) = v>>21;
  } else {
    \*(ptr++) = v | B;
    \*(ptr++) = (v>>7) | B;
    \*(ptr++) = (v>>14) | B;
    \*(ptr++) = (v>>21) | B;
    \*(ptr++) = v>>28;
  }
  return reinterpret_cast<char\*>(ptr);
}

300 = 100101100

由于 300 超过了 7 位(Varint 一个字节只有 7 位能用来表示数字,最高位 msb 用来表示后面是否有更多字节),所以 300 需要用 2 个字节来表示。
Varint 的编码,以 300 举例:

if (v < (1<<14)) {
    \*(ptr++) = v | B;
    \*(ptr++) = v>>7;
}
1. 100101100 | 10000000 = 1 1010 1100
2. 110101100 取出末尾 7 位 = 010 1100
3. 100101100 >> 7 = 10 = 0000 0010
4. 1010 1100 0000 0010 (最终 Varint 结果)

Varint 的解码算法应该是这样的:(实际就是编码的逆过程)

  1. 如果是多个字节,先去掉每个字节的 msb(通过逻辑或运算),每个字节只留下 7 位。
  2. 逆序整个结果,最多是 5 个字节,排序是 1-2-3-4-5,逆序之后就是 5-4-3-2-1,字节内部的二进制位的顺序不变,变的是字节的相对位置。

解码过程调用 GetVarint32Ptr 函数,如果是大于一个字节的情况,会调用 GetVarint32PtrFallback 来处理。

inline const char\* GetVarint32Ptr(const char\* p,
                                  const char\* limit,
                                  uint32\_t\* value) {
  if (p < limit) {
    uint32\_t result = \*(reinterpret_cast<const unsigned char\*>(p));
    if ((result & 128) == 0) {
      \*value = result;
      return p + 1;
    }
  }
  return GetVarint32PtrFallback(p, limit, value);
}
const char\* GetVarint32PtrFallback(const char\* p,
                                   const char\* limit,
                                   uint32\_t\* value) {
  uint32\_t result = 0;
  for (uint32\_t shift = 0; shift <= 28 && p < limit; shift += 7) {
    uint32\_t byte = \*(reinterpret_cast<const unsigned char\*>(p));
    p++;
    if (byte & 128) {


![img](https://img-blog.csdnimg.cn/img_convert/83a592a01cada5515ebb2df7aefe7d0a.png)
![img](https://img-blog.csdnimg.cn/img_convert/c6f04f1ab0d11d9567676ee2aa539dbd.png)
![img](https://img-blog.csdnimg.cn/img_convert/678a3b849042d3211c2871e0194dcb20.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

  return GetVarint32PtrFallback(p, limit, value);
}
const char\* GetVarint32PtrFallback(const char\* p,
                                   const char\* limit,
                                   uint32\_t\* value) {
  uint32\_t result = 0;
  for (uint32\_t shift = 0; shift <= 28 && p < limit; shift += 7) {
    uint32\_t byte = \*(reinterpret_cast<const unsigned char\*>(p));
    p++;
    if (byte & 128) {


[外链图片转存中...(img-BAxDOSlI-1715377618531)]
[外链图片转存中...(img-Rvc2T2dt-1715377618531)]
[外链图片转存中...(img-VSnqbse2-1715377618532)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值