Go 不透明 API:手動遷移

描述手動遷移至不透明 API 的方法。

不透明 API 是 Go 程式設計語言的 Protocol Buffers 實作的最新版本。舊版本現在稱為 Open Struct API。請參閱 Go Protobuf:發布不透明 API 部落格文章以取得簡介。

這是將 Go Protobuf 用法從舊版 Open Struct 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 (舊)不透明 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 (舊) 和不透明 API (新) 之間幾乎 1:1 的轉換。

一般而言,為了提高可讀性,建議使用建構器。只有在極少數情況下,例如在熱門內部迴圈中建立 Protobuf 訊息時,才可能更適合使用 setter 而不是建構器。如需更多詳細資訊,請參閱不透明 API 常見問題:我應該使用建構器還是 setter?

上述範例的例外情況是使用 oneof 時:Open Struct API (舊) 為每個 oneof case 使用包裝器結構類型,而不透明 API (新) 將 oneof 欄位視為一般訊息欄位

Open Struct API (舊)不透明 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 將傳回零長度的 slice (可以是 nil 或空 slice)。使用者應使用 Has 來判斷存在性,而不是依賴 Get 是否傳回 nil。

Has 報告欄位是否已填入。當在 nil 訊息接收器上呼叫時,它會傳回 false。

Clear 清除欄位。當在 nil 訊息接收器上呼叫時,它會 panic。

在 中使用字串欄位的範例程式碼片段

Open Struct API (舊)不透明 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 (舊)不透明 (新)
// 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 標頭的副本。slice 內容的變更在重複欄位中是可觀察到的。因此,如果使用空 slice 呼叫 Set,則立即呼叫 Get 將傳回相同的 slice。對於線路或文字封送處理輸出,傳入的 nil slice 與空 slice 沒有區別。

對於訊息 Concert 上名為 support_acts 的重複訊息類型欄位,將為其產生以下存取器方法

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

範例程式碼片段

Open Struct API (舊)不透明 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

假設有一個訊息使用 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 (舊)不透明 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 產生的不透明 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 欄位產生的不透明 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 訊息 case 欄位上呼叫 Set 會將欄位設定為空訊息。當在 nil 訊息接收器上呼叫時,它會 panic。

Has 報告 case 欄位是否已設定。當在 nil 訊息接收器上呼叫時,它會傳回 false。

Clear 清除 case 欄位。如果先前已設定,也會清除 oneof 聯集。如果 oneof 聯集設定為不同的欄位,則不會清除 oneof 聯集。當在 nil 訊息接收器上呼叫時,它會 panic。

範例程式碼片段

Open Struct API (舊)不透明 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 遷移時,在 proto 訊息類型上使用 Go reflect 套件來存取結構欄位和標籤的程式碼將不再運作。程式碼將需要遷移以使用 protoreflect

某些常見的程式庫在底層使用 Go reflect,範例包括