暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

​protobuf3 中真正可选的标量类型(带有 Go 示例)

云原生CTO 2021-11-25
1078

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]

参考资料

[1]

参考地址: https://iximiuz.com/en/posts/truly-optional-scalar-types-in-protobuf3/


文章转载自云原生CTO,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论