关注微信公众号《云原生CTO》更多云原生干货等你来探索
专注于 云原生技术
分享
提供优质 云原生开发
视频技术培训
面试技巧
,及技术疑难问题 解答

云原生技术分享不仅仅局限于Go
、Rust
、Python
、Istio
、containerd
、CoreDNS
、Envoy
、etcd
、Fluentd
、Harbor
、Helm
、Jaeger
、Kubernetes
、Open Policy Agent
、Prometheus
、Rook
、TiKV
、TUF
、Vitess
、Argo
、Buildpacks
、CloudEvents
、CNI
、Contour
、Cortex
、CRI-O
、Falco
、Flux
、gRPC
、KubeEdge
、Linkerd
、NATS
、Notary
、OpenTracing
、Operator Framework
、SPIFFE
、SPIRE
和 Thanos
等


protobuf3 中真正可选的标量类型(带有 Go 示例)
与protobuf2
相比,protobuf3
中没有办法将某些字段标记为可选字段,而将其他字段标记为必需字段。相反,可以省略任何字段,从而将该字段设置为其默认的零值。我相信做出这样的设计决定有很多好的理由。然而,尽管这种行为可能优于proto2
在必需字段和可选字段之间的显式区别,但它也有一些不幸的含义。

来自proto3
语言指南:
在解析消息时,如果编码的消息不包含特定的奇异元素,则解析对象中的对应字段将被设置为该字段的默认值
对于字符串,默认值是空字符串。 对于字节,默认值为空字节。 对于 bool
类型,默认值为false
。对于数字类型,默认值为零。 对于 enum
,默认值是第一个定义的enum
值,该值必须为0
。对于消息字段,未设置该字段。它的确切值取决于语言。有关详细信息,请参阅生成的代码指南。
注意,对于标量消息字段,一旦消息解析没有告诉一个字段是否显式地设置为默认值(例如一个布尔值是否设置为
false
)或不设置:你应该牢记这一点在定义你的消息类型。例如,如果你不希望某些行为在默认情况下也发生,就不要有一个设置为false时打开某些行为的布尔值。还请注意,如果将标量消息字段设置为默认值,则不会在网络上序列化该值。
有时,字段的缺失与字段的默认值不同。或者可能根本没有默认值。例子:
// person.proto
syntax = "proto3";
package main;
message Person {
int32 age = 1; // in full years
};
编译它:
protoc --go_out=. person.proto
测试程序
// main.go
package main
import "fmt"
func main() {
john := Person{}
fmt.Println(john.Age)
}
运行:
go run person.pb.go main.go
> 0
对于小于一岁的孩子,相应的Person
对象的年龄将等于0
。然而,如果一个人的个人资料只填写了一部分,我们可能根本不知道这个人的年龄。但是,如果我们将这个人的年龄设置为其默认值(对于数字类型为0
),我们将自动将这个人设置为儿童。虽然重新设计我们的领域模型和重构年龄到出生日期可能是一个更好的解决方案,但如果能在协议级别上有一种方法来处理这种情况,那就太好了。
所以,这意味着我们需要一个策略,如何处理真正可选的领域。幸运的是,protobuf3
为来自协议缓冲区知名类型名称空间的标量值类型提供了一个解决方案——包装器类型。这个想法很简单。我们使用Int32Value
字段代替消息中的整数(或浮点数或布尔值等)字段,该字段是对int32
标量值类型的包装。
协议缓冲区知名类型: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf
Int32Value: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Int32Value
// person.proto
syntax = "proto3";
package main;
import "google/protobuf/wrappers.proto";
message Person {
google.protobuf.Int32Value age = 2;
};
包装器使标量类型复合,支持一些行为定制。不同语言的实现可能有所不同,但生成的Go
代码将如下:
// person.bp.go
import (
wrappers "github.com/golang/protobuf/ptypes/wrappers"
)
type Person struct {
Age *wrappers.Int32Value `protobuf:"bytes,2,opt,name=age,proto3" json:"age,omitempty"`
// ...
}
// ...
在构造Person
对象之后,我们将简单地将age
字段设为nil
,因为它是Go
中指针的默认零值。但是现在我们还可以区分字段的值的缺失和它的零值。检查代码片段:
// main.go
package main
import "fmt"
import "github.com/golang/protobuf/ptypes/wrappers"
func main() {
john := Person{}
fmt.Println(john.Age)
alice := Person{Age: &wrappers.Int32Value{Value: 0}}
fmt.Printf("%#v\n", alice.Age)
}
运行它
go run person.pb.go main.go
> <nil>
> &wrappers.Int32Value{Value:0, ...}
因此,如果你不知道一个人的年龄,就跳过这个领域。它将被设置为nil
,在接收端解码后,该值的缺失将会被注意到。但如果你有一个值,即使它可能等于类型的默认值(即0
),你指定它的事实也将是透明的。protobuf
编译器非常聪明,可以为所有受支持的后端处理这种约定。上面的输出表明,对于Go
包装器来说,它们基本上是带有相应标量类型的字段Value
的结构体。
好处:JSON的互操作性
Protobuf
正在努力实现不同编程语言之间以及异构系统之间的最大互操作性。它为消息的JSON
编码提供了第一类支持。特别是对于Go
,可以注意到.pb
中生成的JSON
注释。进入文件(参见上面的例子)。然而,当涉及到包装类型时,默认的Go encoding/json
包不能很好地工作:
第一类支持: https://developers.google.com/protocol-buffers/docs/proto3#json
// main.go
package main
import "encoding/json"
import "fmt"
import "github.com/golang/protobuf/ptypes/wrappers"
func main() {
alice := Person{Age: &wrappers.Int32Value{Value: 0}}
bytes, _ := json.Marshal(alice)
fmt.Println(string(bytes))
bob := Person{Age: &wrappers.Int32Value{Value: 42}}
bytes, _ = json.Marshal(bob)
fmt.Println(string(bytes))
}
运行它:
go run person.pb.go main.go
> {"age":{}}
> {"age":{"value":42}}
上面生成的JSON
看起来不有效,因为我们用的是对象而不是整数字段。要解决这个问题,需要使用Go
协议缓冲区后端提供的jsonpb
包:
// main.go
package main
import "fmt"
import "github.com/golang/protobuf/jsonpb"
import "github.com/golang/protobuf/ptypes/wrappers"
func main() {
alice := Person{Age: &wrappers.Int32Value{Value: 0}}
bob := Person{Age: &wrappers.Int32Value{Value: 42}}
m := jsonpb.Marshaler{}
a, _ := m.MarshalToString(&alice)
fmt.Println(a)
b, _ := m.MarshalToString(&bob)
fmt.Println(b)
}
运行它:
go run person.pb.go main.go
> {"age":0}
> {"age":42}
这个包知道包装器的目的,可以为包装的标量字段生成有效的JSON
。
参考地址[1]
参考资料
参考地址: https://iximiuz.com/en/posts/truly-optional-scalar-types-in-protobuf3/




