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 和新 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 訊息時,才可能更喜歡使用 setter 而不是構建器。有關更多詳細資訊,請參閱Opaque API FAQ: 我應該使用構建器還是 setter?

上述示例的一個例外是處理 oneofs 時:Open Struct API (舊) 對每個 oneof 情況使用包裝器結構型別,而 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 情況欄位,則最後一個(在 .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,則返回預設值。預設值是零值,除非使用預設選項顯式設定。

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)

對映

假設有一個定義了對映型別欄位的訊息

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

對映欄位將具有 GetSet 方法。

Get 返回欄位的值。如果未設定欄位或訊息接收器為 nil,則返回 nil。

Set 將提供的值儲存到欄位中。在 nil 訊息接收器上呼叫時會 panic。 Set 將儲存提供的對映引用的副本。提供的對映的更改在對映欄位中是可觀察的。

對於訊息 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)

Oneof

對於每個 oneof 聯合分組,訊息上將有一個 WhichHasClear 方法。在該聯合中的每個 oneof 情況欄位上也將有一個 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 透過返回欄位編號報告設定了哪個情況欄位。當未設定任何欄位或在 nil 訊息接收器上呼叫時返回 0。

Has 報告 oneof 中的任何欄位是否已設定。在 nil 訊息接收器上呼叫時返回 false。

Clear 清除 oneof 中當前設定的情況欄位。在 nil 訊息接收器上呼叫時會 panic。

為每個 oneof 情況欄位生成的 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 返回情況欄位的值。如果未設定情況欄位或在 nil 訊息接收器上呼叫時,它將返回零值。

Set 將提供的值儲存到情況欄位中。它還隱式清除 oneof 聯合中以前填充的情況欄位。使用 nil 值呼叫 oneof 訊息情況欄位上的 Set 會將欄位設定為空訊息。在 nil 訊息接收器上呼叫時會 panic。

Has 報告情況欄位是否已設定。在 nil 訊息接收器上呼叫時返回 false。

Clear 清除情況欄位。如果以前已設定,則 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())
}

反射

在 proto 訊息型別上使用 Go reflect 包訪問結構欄位和標籤的程式碼在從 Open Struct API 遷移後將不再起作用。程式碼將需要遷移到使用 protoreflect

一些常用庫在底層確實使用了 Go reflect,例如