cover

思考 ProtoBuf 中的可选与必选字段

sorcererxw

没有 optional 的世界

在很长的一段时间里,ProtoBuf3 不支持 ProtoBuf2 当中 optional 关键字。正如我在gRPC 与『面向扩展编程』 中提到的,我认为所有字段均为可选,有益于未来的扩展性,避免加入一个新的字段,导致调用方的代码类型出错。

比如在 TypeScript 当中,如果一个字段未修饰为 optional,那么初始化这个 object 就必须要完整的定义所有字段。如果有无数个调用方在使用这个 Request 类型,显然就很难在其中增加字段了。

一般来说,这个时候我们希望让一个字段 nullable 有两种方法:

  • 使用 oneof 大法。
    message Foo {
        int32 bar = 1;
        oneof optional_baz {
            int32 baz = 2;
        }
    }
  • 使用 google.protobuf.wrappers ,作为基础类型的包装类,在基础数值上引入 null 的状态,就像 Java 当中 Integer 与 int 的关系。
    message Foo {
        int32 bar = 1;
        google.protobuf.Int32Value baz = 2;
    }

上面的方法虽然不是很优雅,但可以 work!!!

重新引入 optional

不过最近 ProtoBuf v3.15.0 正式重新引入了 optional 关键字,着实令我有点 mind blowing。

Release Protocol Buffers v3.15.0 · protocolbuffers/protobuf
Protocol Compiler Optional fields for proto3 are enabled by default, and no longer require the --experimental_allow_proto3_optional flag. C++ MessageDifferencer: fixed bug when using custom igno...
https://github.com/protocolbuffers/protobuf/releases/tag/v3.15.0
syntax = "proto3";

message Request {
	string arg1 = 1;
	optional string arg2 = 2;
}
那么不带 optional 的字段到底是可选还是必选?

带着这个疑问,基于 TypeScript 和 Go 的 gRPC 做了一些实验,optional 和 non-optional 字段的不同表现,总结出如下的表现:

  • 普通字段:
    • Go 当中会使用基础类型表示,默认就具有了零值。
    • TypeScript 当中也会将其作为基础类型表示,虽然 Node 中没有默认零值,但是 gRPC client 和 server 都会在接收到 proto message 的时候将所有未定义的字段初始化为零值。
  • optional 字段:
    • Go 当中会使用指针类型 field *T 表示,不再具有零值。
    • TypeScript 当中也会将其作为可选类型 field?: T 表示,不需要去初始化。

这样就很清晰了,在没有 optional 的时候,所有字段并不是真正意义上的可选字段,只是对于调用方来说,可以选择性地初始化部分字段,但是对于被调用方来说,需要依赖框架或者语言本身的特性去补齐为定义字段,这样在使用的时候才不会产生空指针错误。

如果一个字段是可选字段,明确有一个空的状态,这个状态需要调用和被调用双方都能感知,就需要将其标记为 optional。