Go Opaque API:手动迁移

描述了手动迁移到 Opaque API 的过程。

Opaque API 是 Go 编程语言的 Protocol Buffers 实现的最新版本。旧版本现在称为 Open Struct API。请参阅Go Protobuf:发布 Opaque API 这篇博文以了解介绍。

本文是关于如何将 Go Protobuf 的使用从旧的 Open Struct API 迁移到新的 Opaque API 的用户指南。

生成的代码指南提供了更多细节。本指南将新旧 API 进行对比。

消息构造

假设定义了一个 Protobuf 消息如下所示

message Foo {
  uint32 uint32 = 1;
  bytes bytes = 2;
  oneof union {
    string    string = 4;
    MyMessage message = 5;
  }
  enum Kind {  };
  Kind kind = 9;
}

以下是如何从字面值构造此消息的示例

Open Struct API (旧)Opaque API (新)
m := &pb.Foo{
  Uint32: proto.Uint32(5),
  Bytes:  []byte("hello"),
}
m := pb.Foo_builder{
  Uint32: proto.Uint32(5),
  Bytes:  []byte("hello"),
}.Build()

如您所见,构建器结构体允许 Open Struct API (旧) 和 Opaque API (新) 之间进行几乎 1:1 的转换。

通常,为了可读性,首选使用构建器。只有在极少数情况下,例如在热点内部循环中创建 Protobuf 消息时,使用 setters 可能比使用 builders 更好。有关更多详细信息,请参阅Opaque API 常见问题:我应该使用 builders 还是 setters?

上述示例的一个例外是处理 oneofs 时:Open Struct API (旧) 为每个 oneof case 使用一个包装器结构体类型,而 Opaque API (新) 将 oneof 字段视为常规消息字段。

Open Struct API (旧)Opaque API (新)
m := &pb.Foo{
  Uint32: myScalar,  // could be nil
  Union:  &pb.Foo_String{myString},
  Kind:   pb.Foo_SPECIAL_KIND.Enum(),
}
m := pb.Foo_builder{
  Uint32: myScalar,
  String: myString,
  Kind:   pb.Foo_SPECIAL_KIND.Enum(),
}.Build()

对于与 oneof 联合关联的一组 Go 结构体字段,只能填充一个字段。如果填充了多个 oneof case 字段,则最后一个(按照 .proto 文件中的字段声明顺序)生效。

标量字段

假设定义了一个包含标量字段的消息

message Artist {
  int32 birth_year = 1;
}

Go 中使用标量类型(bool, int32, int64, uint32, uint64, float32, float64, string, []byte, 和 enum)的 Protobuf 消息字段将具有 GetSet 访问器方法。具有显式存在性的字段还将具有 HasClear 方法。

对于一个类型为 int32 名为 birth_year 的字段,将为其生成以下访问器方法

func (m *Artist) GetBirthYear() int32
func (m *Artist) SetBirthYear(v int32)
func (m *Artist) HasBirthYear() bool
func (m *Artist) ClearBirthYear()

Get 返回字段的值。如果字段未设置或消息接收者为 nil,则返回默认值。默认值为 零值,除非使用 default 选项显式设置。

Set 将提供的值存储到字段中。当在 nil 消息接收者上调用时,它会 panic。

对于字节字段,使用 nil []byte 调用 Set 将被视为已设置。例如,紧随其后调用 Has 返回 true。紧随其后调用 Get 将返回一个零长度的 slice(可以是 nil 或空 slice)。用户应该使用 Has 来确定存在性,而不是依赖于 Get 是否返回 nil。

Has 报告字段是否已填充。当在 nil 消息接收者上调用时,它返回 false。

Clear 清除字段。当在 nil 消息接收者上调用时,它会 panic。

使用字符串字段的代码片段示例位于

Open Struct API (旧)Opaque API (新)
// Getting the value.
s := m.GetBirthYear()

// Setting the field.
m.BirthYear = proto.Int32(1989)

// Check for presence.
if s.BirthYear != nil {  }

// Clearing the field.
m.BirthYear = nil
// Getting the field value.
s := m.GetBirthYear()

// Setting the field.
m.SetBirthYear(1989)

// Check for presence.
if m.HasBirthYear() {  }

// Clearing the field
m.ClearBirthYear()

消息字段

假设定义了一个包含消息类型字段的消息

message Band {}

message Concert {
  Band headliner = 1;
}

消息类型的 Protobuf 消息字段将具有 Get, Set, HasClear 方法。

对于一个消息类型名名为 headliner 的字段,将为其生成以下访问器方法

func (m *Concert) GetHeadliner() *Band
func (m *Concert) SetHeadliner(*Band)
func (m *Concert) HasHeadliner() bool
func (m *Concert) ClearHeadliner()

Get 返回字段的值。如果未设置或在 nil 消息接收者上调用时,它返回 nil。检查 Get 是否返回 nil 等同于检查 Has 是否返回 false。

Set 将提供的值存储到字段中。当在 nil 消息接收者上调用时,它会 panic。使用 nil 指针调用 Set 等同于调用 Clear

Has 报告字段是否已填充。当在 nil 消息接收者上调用时,它返回 false。

Clear 清除字段。当在 nil 消息接收者上调用时,它会 panic。

代码片段示例

Open Struct API (旧)Opaque (新)
// Getting the value.
b := m.GetHeadliner()

// Setting the field.
m.Headliner = &pb.Band{}

// Check for presence.
if s.Headliner != nil {  }

// Clearing the field.
m.Headliner = nil
// Getting the value.
s := m.GetHeadliner()

// Setting the field.
m.SetHeadliner(&pb.Band{})

// Check for presence.
if m.HasHeadliner() {  }

// Clearing the field
m.ClearHeadliner()

重复字段

假设定义了一个包含重复消息类型字段的消息

message Concert {
  repeated Band support_acts = 2;
}

重复字段将具有 GetSet 方法。

Get 返回字段的值。如果字段未设置或消息接收者为 nil,则返回 nil。

Set 将提供的值存储到字段中。当在 nil 消息接收者上调用时,它会 panic。 Set 将存储所提供的 slice header 的副本。对 slice 内容的更改在重复字段中是可见的。因此,如果使用空 slice 调用 Set,则紧随其后调用 Get 将返回相同的 slice。对于 wire 或文本 marshaling 输出,传入的 nil slice 与空 slice 没有区别。

对于消息 Concert 中名为 support_acts 的重复消息类型字段,将为其生成以下访问器方法

func (m *Concert) GetSupportActs() []*Band
func (m *Concert) SetSupportActs([]*Band)

代码片段示例

Open Struct API (旧)Opaque API (新)
// Getting the entire repeated value.
v := m.GetSupportActs()

// Setting the field.
m.SupportActs = v

// Get an element in a repeated field.
e := m.SupportActs[i]

// Set an element in a repeated field.
m.SupportActs[i] = e

// Get the length of a repeated field.
n := len(m.GetSupportActs())

// Truncate a repeated field.
m.SupportActs = m.SupportActs[:i]

// Append to a repeated field.
m.SupportActs = append(m.GetSupportActs(), e)
m.SupportActs = append(m.GetSupportActs(), v...)

// Clearing the field.
m.SupportActs = nil
// Getting the entire repeated value.
v := m.GetSupportActs()

// Setting the field.
m.SetSupportActs(v)

// Get an element in a repeated field.
e := m.GetSupportActs()[i]

// Set an element in a repeated field.
m.GetSupportActs()[i] = e

// Get the length of a repeated field.
n := len(m.GetSupportActs())

// Truncate a repeated field.
m.SetSupportActs(m.GetSupportActs()[:i])

// Append to a repeated field.
m.SetSupportActs(append(m.GetSupportActs(), e))
m.SetSupportActs(append(m.GetSupportActs(), v...))

// Clearing the field.
m.SetSupportActs(nil)

映射

假设定义了一个包含映射类型字段的消息

message MerchBooth {
  map<string, MerchItems> items = 1;
}

映射字段将具有 GetSet 方法。

Get 返回字段的值。如果字段未设置或消息接收者为 nil,则返回 nil。

Set 将提供的值存储到字段中。当在 nil 消息接收者上调用时,它会 panic。 Set 将存储所提供的 map reference 的副本。对所提供 map 的更改在 map 字段中是可见的。

对于消息 MerchBooth 中名为 items 的映射字段,将为其生成以下访问器方法

func (m *MerchBooth) GetItems() map[string]*MerchItem
func (m *MerchBooth) SetItems(map[string]*MerchItem)

代码片段示例

Open Struct API (旧)Opaque API (新)
// Getting the entire map value.
v := m.GetItems()

// Setting the field.
m.Items = v

// Get an element in a map field.
v := m.Items[k]

// Set an element in a map field.
// This will panic if m.Items is nil.
// You should check m.Items for nil
// before doing the assignment to ensure
// it won't panic.
m.Items[k] = v

// Delete an element in a map field.
delete(m.Items, k)

// Get the size of a map field.
n := len(m.GetItems())

// Clearing the field.
m.Items = nil
// Getting the entire map value.
v := m.GetItems()

// Setting the field.
m.SetItems(v)

// Get an element in a map field.
v := m.GetItems()[k]

// Set an element in a map field.
// This will panic if m.GetItems() is nil.
// You should check m.GetItems() for nil
// before doing the assignment to ensure
// it won't panic.
m.GetItems()[k] = v

// Delete an element in a map field.
delete(m.GetItems(), k)

// Get the size of a map field.
n := len(m.GetItems())

// Clearing the field.
m.SetItems(nil)

Oneofs

对于每个 oneof 联合分组,消息上将有一个 Which, HasClear 方法。该联合中的每个 oneof case 字段也将有一个 Get, Set, HasClear 方法。

假设定义了一个消息,其中 oneof avatar 中包含 oneof 字段 image_urlimage_data,如下所示

message Profile {
  oneof avatar {
    string image_url = 1;
    bytes image_data = 2;
  }
}

为此 oneof 生成的 Opaque API 将是

func (m *Profile) WhichAvatar() case_Profile_Avatar {  }
func (m *Profile) HasAvatar() bool {  }
func (m *Profile) ClearAvatar() {  }

type case_Profile_Avatar protoreflect.FieldNumber

const (
  Profile_Avatar_not_set_case case_Profile_Avatar = 0
  Profile_ImageUrl_case case_Profile_Avatar = 1
  Profile_ImageData_case case_Profile_Avatar = 2
)

Which 通过返回字段编号报告哪个 case 字段已设置。当未设置任何字段或在 nil 消息接收者上调用时,它返回 0。

Has 报告 oneof 中的任何字段是否已设置。当在 nil 消息接收者上调用时,它返回 false。

Clear 清除 oneof 中当前设置的 case 字段。在 nil 消息接收者上调用时,它会 panic。

为每个 oneof case 字段生成的 Opaque API 将是

func (m *Profile) GetImageUrl() string {  }
func (m *Profile) GetImageData() []byte {  }

func (m *Profile) SetImageUrl(v string) {  }
func (m *Profile) SetImageData(v []byte) {  }

func (m *Profile) HasImageUrl() bool {  }
func (m *Profile) HasImageData() bool {  }

func (m *Profile) ClearImageUrl() {  }
func (m *Profile) ClearImageData() {  }

Get 返回 case 字段的值。如果 case 字段未设置或在 nil 消息接收者上调用时,它将返回零值。

Set 将提供的值存储到 case 字段中。它还会隐式清除先前在 oneof 联合中填充的 case 字段。使用 nil 值在 oneof message case 字段上调用 Set 将把字段设置为一个空消息。当在 nil 消息接收者上调用时,它会 panic。

Has 报告 case 字段是否已设置。当在 nil 消息接收者上调用时,它返回 false。

Clear 清除 case 字段。如果之前已设置,则 oneof 联合也会被清除。如果 oneof 联合设置为不同的字段,则不会清除 oneof 联合。当在 nil 消息接收者上调用时,它会 panic。

代码片段示例

Open Struct API (旧)Opaque API (新)
// Getting the oneof field that is set.
switch m.GetAvatar().(type) {
case *pb.Profile_ImageUrl:
   = m.GetImageUrl()
case *pb.Profile_ImageData:
   = m.GetImageData()
}

// Setting the fields.
m.Avatar = &pb.Profile_ImageUrl{"http://"}
m.Avatar = &pb.Profile_ImageData{img}

// Checking whether any oneof field is set
if m.Avatar != nil {  }

// Clearing the field.
m.Avatar = nil

// Checking if a specific field is set.
_, ok := m.GetAvatar().(*pb.Profile_ImageUrl)
if ok {  }

// Clearing a specific field
_, ok := m.GetAvatar().(*pb.Profile_ImageUrl)
if ok {
  m.Avatar = nil
}

// Copy a oneof field.
m.Avatar = src.Avatar
// Getting the oneof field that is set.
switch m.WhichAvatar() {
case pb.Profile_ImageUrl_case:
   = m.GetImageUrl()
case pb.Profile_ImageData_case:
   = m.GetImageData()
}

// Setting the fields.
m.SetImageUrl("http://")
m.SetImageData([]byte("…"))

// Checking whether any oneof field is set
if m.HasAvatar() {  }

// Clearing the field.
m.ClearAvatar()

// Checking if a specific field is set.
if m.HasImageUrl() {  }

// Clearing a specific field.
m.ClearImageUrl()

// Copy a oneof field
switch src.WhichAvatar() {
case pb.Profile_ImageUrl_case:
  m.SetImageUrl(src.GetImageUrl())
case pb.Profile_ImageData_case:
  m.SetImageData(src.GetImageData())
}

反射

使用 Go reflect 包在 proto 消息类型上访问结构体字段和标签的代码在从 Open Struct API 迁移后将不再工作。代码需要迁移以使用 protoreflect

一些常用库在底层使用 Go reflect,例如