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()

如您所见,构建器结构体 (builder struct) 使得从 Open Struct API(旧)到 Opaque API(新)的转换几乎可以 1:1 实现。

通常,为了可读性,应优先使用构建器。只有在极少数情况下,例如在性能敏感的内部循环中创建 Protobuf 消息时,才可能更适合使用 setter 而非构建器。有关更多详细信息,请参阅Opaque API 常见问题解答:我应该使用构建器还是 setter?

上述示例的一个例外是处理 oneof 时: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 方法。

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

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 将返回一个零长度的切片(可以是 nil 或空切片)。用户应使用 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 字段将具有 GetSetHasClear 方法。

对于一个名为 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 将存储所提供的切片头的副本。对切片内容的更改在重复字段中是可见的。因此,如果使用空切片调用 Set,之后立即调用 Get 将返回相同的切片。对于有线格式或文本格式的编组输出,传入的 nil 切片与空切片无法区分。

对于 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)

映射

假设有一个定义了 map 类型字段的消息:

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

Map 字段将具有 GetSet 方法。

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

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

对于 MerchBooth 消息上一个名为 items 的 map 字段,将为其生成以下访问器方法:

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)

Oneof

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

假设有一个定义了 oneof 字段 image_urlimage_data 的消息,它们在 oneof avatar 中,如下所示:

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 字段。对 oneof 消息 case 字段使用 nil 值调用 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())
}

反射

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

一些常用库在底层确实使用了 Go reflect,例如: