Go 程式碼產生指南 (Open)

說明 protocol buffer 編譯器針對任何給定的協定定義所產生的 Go 程式碼。

所有 proto2 和 proto3 產生程式碼之間的差異都會特別標示出來 - 請注意,這些差異在於本文件中描述的產生程式碼,而不是基礎 API,兩者在兩個版本中是相同的。在閱讀本文檔之前,您應該先閱讀 proto2 語言指南 和/或 proto3 語言指南

編譯器調用

protocol buffer 編譯器需要外掛程式才能產生 Go 程式碼。使用 Go 1.16 或更高版本執行以下命令進行安裝

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

這將在 $GOBIN 中安裝 protoc-gen-go 二進位檔案。設定 $GOBIN 環境變數以變更安裝位置。它必須在您的 $PATH 中,protocol buffer 編譯器才能找到它。

當使用 go_out 旗標調用時,protocol buffer 編譯器會產生 Go 輸出。go_out 旗標的引數是您希望編譯器寫入 Go 輸出的目錄。編譯器會為每個 .proto 檔案輸入建立單個原始碼檔案。輸出檔案的名稱是透過將 .proto 副檔名替換為 .pb.go 來建立的。

產生的 .pb.go 檔案放置在輸出目錄中的位置取決於編譯器旗標。有幾種輸出模式

  • 如果指定了 paths=import 旗標,則輸出檔案會放置在以 Go 套件的匯入路徑命名的目錄中 (例如 .proto 檔案中 go_package 選項提供的路徑)。例如,輸入檔案 protos/buzz.proto,Go 匯入路徑為 example.com/project/protos/fizz,會在 example.com/project/protos/fizz/buzz.pb.go 產生輸出檔案。如果未指定 paths 旗標,這是預設輸出模式。
  • 如果指定了 module=$PREFIX 旗標,則輸出檔案會放置在以 Go 套件的匯入路徑命名的目錄中 (例如 .proto 檔案中 go_package 選項提供的路徑),但輸出檔案名稱中會移除指定的目錄前綴。例如,輸入檔案 protos/buzz.proto,Go 匯入路徑為 example.com/project/protos/fizz,且將 example.com/project 指定為 module 前綴,會在 protos/fizz/buzz.pb.go 產生輸出檔案。在模組路徑之外產生任何 Go 套件都會導致錯誤。此模式適用於將產生的檔案直接輸出到 Go 模組中。
  • 如果指定了 paths=source_relative 旗標,則輸出檔案會放置在與輸入檔案相同的相對目錄中。例如,輸入檔案 protos/buzz.proto 會在 protos/buzz.pb.go 產生輸出檔案。

protoc-gen-go 特有的旗標是透過在調用 protoc 時傳遞 go_opt 旗標來提供的。可以傳遞多個 go_opt 旗標。例如,當執行

protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto

編譯器將從 src 目錄中讀取輸入檔案 foo.protobar/baz.proto,並將輸出檔案 foo.pb.gobar/baz.pb.go 寫入 out 目錄。如果需要,編譯器會自動建立巢狀輸出子目錄,但不會建立輸出目錄本身。

套件

為了產生 Go 程式碼,必須為每個 .proto 檔案 (包括那些被產生的 .proto 檔案以遞移方式依賴的檔案) 提供 Go 套件的匯入路徑。有兩種方法可以指定 Go 匯入路徑

  • .proto 檔案中宣告,或
  • 在調用 protoc 時在命令列上宣告。

我們建議在 .proto 檔案中宣告,以便 .proto 檔案的 Go 套件可以集中識別 .proto 檔案本身,並簡化調用 protoc 時傳遞的旗標集。如果給定 .proto 檔案的 Go 匯入路徑由 .proto 檔案本身和命令列提供,則後者優先於前者。

Go 匯入路徑在 .proto 檔案中以本機方式指定,方法是宣告具有 Go 套件完整匯入路徑的 go_package 選項。範例用法

option go_package = "example.com/project/protos/fizz";

Go 匯入路徑可以在調用編譯器時在命令列上指定,方法是傳遞一個或多個 M${PROTO_FILE}=${GO_IMPORT_PATH} 旗標。範例用法

protoc --proto_path=src \
  --go_opt=Mprotos/buzz.proto=example.com/project/protos/fizz \
  --go_opt=Mprotos/bar.proto=example.com/project/protos/foo \
  protos/buzz.proto protos/bar.proto

由於所有 .proto 檔案到其 Go 匯入路徑的對應可能很大,因此指定 Go 匯入路徑的這種模式通常由某些建置工具 (例如,Bazel) 執行,這些工具可以控制整個依賴樹。如果給定 .proto 檔案有重複的項目,則最後指定的項目優先。

對於 go_package 選項和 M 旗標,該值可以包含以分號與匯入路徑分隔的明確套件名稱。例如:"example.com/protos/foo;package_name"。不建議使用此用法,因為套件名稱預設會以合理的方式從匯入路徑衍生。

匯入路徑用於確定當一個 .proto 檔案匯入另一個 .proto 檔案時,必須產生哪些匯入陳述式。例如,如果 a.proto 匯入 b.proto,則產生的 a.pb.go 檔案需要匯入包含產生的 b.pb.go 檔案的 Go 套件 (除非兩個檔案都在同一個套件中)。匯入路徑也用於建構輸出檔案名稱。請參閱上面的「編譯器調用」章節以瞭解詳細資訊。

Go 匯入路徑與 .proto 檔案中的 package 指定符 之間沒有關聯性。後者僅與 protobuf 命名空間相關,而前者僅與 Go 命名空間相關。此外,Go 匯入路徑與 .proto 匯入路徑之間也沒有關聯性。

API 層級

產生的程式碼使用 Open Struct API 或 Opaque API。請參閱 Go Protobuf:新的 Opaque API 部落格文章以取得介紹。

根據您的 .proto 檔案使用的語法,以下是將使用的 API

.proto 語法API 層級
proto2Open Struct API
proto3Open Struct API
版本 2023Open Struct API
版本 2024+Opaque API

您可以透過在 .proto 檔案中設定 api_level 版本特性來選取 API。這可以針對每個檔案或每個訊息設定

edition = "2023";

package log;

import "google/protobuf/go_features.proto";
option features.(pb.go).api_level = API_OPAQUE;

message LogEntry {  }

為了您的方便,您也可以使用 protoc 命令列旗標覆寫預設 API 層級

protoc […] --go_opt=default_api_level=API_HYBRID

若要覆寫特定檔案 (而不是所有檔案) 的預設 API 層級,請使用 apilevelM 對應旗標 (類似於 匯入路徑的 M 旗標)

protoc […] --go_opt=apilevelMhello.proto=API_HYBRID

命令列旗標也適用於仍使用 proto2 或 proto3 語法的 .proto 檔案,但如果您想從 .proto 檔案中選取 API 層級,則需要先將所述檔案遷移到版本。

訊息

假設有一個簡單的訊息宣告

message Artist {}

protocol buffer 編譯器會產生一個名為 Artist 的結構。*Artist 實作了 proto.Message 介面。

proto 套件 提供了對訊息進行操作的函數,包括與二進位格式之間的轉換。

proto.Message 介面定義了一個 ProtoReflect 方法。此方法會傳回一個 protoreflect.Message,它提供了訊息的反射式檢視。

optimize_for 選項不會影響 Go 程式碼產生器的輸出。

當多個 goroutine 同時存取同一個訊息時,適用以下規則

  • 同時存取 (讀取) 欄位是安全的,但有一個例外
  • 修改同一個訊息中的不同欄位是安全的。
  • 同時修改一個欄位是不安全的。
  • 以任何方式同時修改訊息與 proto 套件 的函數 (例如 proto.Marshalproto.Size) 是不安全的。

巢狀類型

訊息可以在另一個訊息內部宣告。例如

message Artist {
  message Name {
  }
}

在這種情況下,編譯器會產生兩個結構:ArtistArtist_Name

欄位

protocol buffer 編譯器會為訊息中定義的每個欄位產生一個結構欄位。此欄位的確切性質取決於其類型以及它是單數、重複、map 或 oneof 欄位。

請注意,產生的 Go 欄位名稱始終使用駝峰式命名,即使 .proto 檔案中的欄位名稱使用帶底線的小寫 (應該如此)。大小寫轉換的工作方式如下

  1. 第一個字母會大寫以進行匯出。如果第一個字元是底線,則會移除它並在前面加上大寫 X。
  2. 如果內部底線後跟小寫字母,則會移除底線,並將後面的字母大寫。

因此,proto 欄位 birth_year 在 Go 中變成 BirthYear,而 _birth_year_2 變成 XBirthYear_2

單數純量欄位 (proto2)

對於以下任一欄位定義

optional int32 birth_year = 1;
required int32 birth_year = 1;

編譯器會產生一個結構,其中包含名為 BirthYear*int32 欄位和一個存取器方法 GetBirthYear(),該方法傳回 Artist 中的 int32 值,如果欄位未設定,則傳回預設值。如果未明確設定預設值,則會改用該類型的 零值 (數字為 0,字串為空字串)。

對於其他純量欄位類型 (包括 boolbytesstring),*int32 會根據 純量值類型表 替換為對應的 Go 類型。

單數純量欄位 (proto3)

對於此欄位定義

int32 birth_year = 1;
optional int32 first_active_year = 2;

編譯器將產生一個結構,其中包含名為 BirthYearint32 欄位和一個存取器方法 GetBirthYear(),該方法傳回 birth_year 中的 int32 值,如果欄位未設定,則傳回該類型的 零值 (數字為 0,字串為空字串)。

FirstActiveYear 結構欄位的類型將為 *int32,因為它標記為 optional

對於其他純量欄位類型 (包括 boolbytesstring),int32 會根據 純量值類型表 替換為對應的 Go 類型。proto 中的未設定值將表示為該類型的 零值 (數字為 0,字串為空字串)。

單數訊息欄位

給定訊息類型

message Band {}

對於具有 Band 欄位的訊息

// proto2
message Concert {
  optional Band headliner = 1;
  // The generated code is the same result if required instead of optional.
}

// proto3
message Concert {
  Band headliner = 1;
}

編譯器將產生一個 Go 結構

type Concert struct {
    Headliner *Band
}

訊息欄位可以設定為 nil,這表示欄位未設定,有效地清除欄位。這不等同於將值設定為訊息結構的「空」實例。

編譯器還會產生一個 func (m *Concert) GetHeadliner() *Band 輔助函數。如果 m 為 nil 或 headliner 未設定,此函數會傳回 nil *Band。這使得可以鏈式呼叫 get,而無需中間 nil 檢查

var m *Concert // defaults to nil
log.Infof("GetFoundingYear() = %d (no panic!)", m.GetHeadliner().GetFoundingYear())

重複欄位

每個重複欄位都會在 Go 的結構中產生一個 T 欄位的切片,其中 T 是欄位的元素類型。對於具有重複欄位的此訊息

message Concert {
  // Best practice: use pluralized names for repeated fields:
  // /programming-guides/style#repeated-fields
  repeated Band support_acts = 1;
}

編譯器會產生 Go 結構

type Concert struct {
    SupportActs []*Band
}

同樣地,對於欄位定義 repeated bytes band_promo_images = 1;,編譯器將產生一個 Go 結構,其中包含名為 BandPromoImage[][]byte 欄位。對於重複的 列舉,例如 repeated MusicGenre genres = 2;,編譯器會產生一個結構,其中包含名為 Genre[]MusicGenre 欄位。

以下範例顯示如何設定欄位

concert := &Concert{
  SupportActs: []*Band{
    {}, // First element.
    {}, // Second element.
  },
}

若要存取欄位,您可以執行以下操作

support := concert.GetSupportActs() // support type is []*Band.
b1 := support[0] // b1 type is *Band, the first element in support_acts.

Map 欄位

每個 map 欄位都會在結構中產生一個 map[TKey]TValue 類型的欄位,其中 TKey 是欄位的金鑰類型,TValue 是欄位的值類型。對於具有 map 欄位的此訊息

message MerchItem {}

message MerchBooth {
  // items maps from merchandise item name ("Signed T-Shirt") to
  // a MerchItem message with more details about the item.
  map<string, MerchItem> items = 1;
}

編譯器會產生 Go 結構

type MerchBooth struct {
    Items map[string]*MerchItem
}

Oneof 欄位

對於 oneof 欄位,protobuf 編譯器會產生一個具有介面類型 isMessageName_MyField 的單個欄位。它還會為 oneof 中的每個 單數欄位 產生一個結構。這些結構都實作了此 isMessageName_MyField 介面。

對於具有 oneof 欄位的此訊息

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

編譯器會產生結構

type Profile struct {
    // Types that are valid to be assigned to Avatar:
    //  *Profile_ImageUrl
    //  *Profile_ImageData
    Avatar isProfile_Avatar `protobuf_oneof:"avatar"`
}

type Profile_ImageUrl struct {
        ImageUrl string
}
type Profile_ImageData struct {
        ImageData []byte
}

*Profile_ImageUrl*Profile_ImageData 都透過提供空的 isProfile_Avatar() 方法來實作 isProfile_Avatar

以下範例顯示如何設定欄位

p1 := &account.Profile{
  Avatar: &account.Profile_ImageUrl{ImageUrl: "http://example.com/image.png"},
}

// imageData is []byte
imageData := getImageData()
p2 := &account.Profile{
  Avatar: &account.Profile_ImageData{ImageData: imageData},
}

若要存取欄位,您可以使用類型切換在值上處理不同的訊息類型。

switch x := m.Avatar.(type) {
case *account.Profile_ImageUrl:
    // Load profile image based on URL
    // using x.ImageUrl
case *account.Profile_ImageData:
    // Load profile image based on bytes
    // using x.ImageData
case nil:
    // The field is not set.
default:
    return fmt.Errorf("Profile.Avatar has unexpected type %T", x)
}

編譯器還會產生 get 方法 func (m *Profile) GetImageUrl() stringfunc (m *Profile) GetImageData() []byte。每個 get 函數都會傳回該欄位的值,如果未設定,則傳回零值。

列舉

給定一個列舉,例如

message Venue {
  enum Kind {
    KIND_UNSPECIFIED = 0;
    KIND_CONCERT_HALL = 1;
    KIND_STADIUM = 2;
    KIND_BAR = 3;
    KIND_OPEN_AIR_FESTIVAL = 4;
  }
  Kind kind = 1;
  // ...
}

protocol buffer 編譯器會產生一個類型和一系列具有該類型的常數

type Venue_Kind int32

const (
    Venue_KIND_UNSPECIFIED       Venue_Kind = 0
    Venue_KIND_CONCERT_HALL      Venue_Kind = 1
    Venue_KIND_STADIUM           Venue_Kind = 2
    Venue_KIND_BAR               Venue_Kind = 3
    Venue_KIND_OPEN_AIR_FESTIVAL Venue_Kind = 4
)

對於訊息內的列舉 (如上例所示),類型名稱以訊息名稱開頭

type Venue_Kind int32

對於套件層級列舉

enum Genre {
  GENRE_UNSPECIFIED = 0;
  GENRE_ROCK = 1;
  GENRE_INDIE = 2;
  GENRE_DRUM_AND_BASS = 3;
  // ...
}

Go 類型名稱與 proto 列舉名稱未修改

type Genre int32

此類型具有 String() 方法,該方法會傳回給定值的名稱。

Enum() 方法會使用給定的值初始化新配置的記憶體,並傳回對應的指標

func (Genre) Enum() *Genre

protocol buffer 編譯器會為列舉中的每個值產生一個常數。對於訊息內的列舉,常數以封閉訊息的名稱開頭

const (
    Venue_KIND_UNSPECIFIED       Venue_Kind = 0
    Venue_KIND_CONCERT_HALL      Venue_Kind = 1
    Venue_KIND_STADIUM           Venue_Kind = 2
    Venue_KIND_BAR               Venue_Kind = 3
    Venue_KIND_OPEN_AIR_FESTIVAL Venue_Kind = 4
)

對於套件層級列舉,常數改為以列舉名稱開頭

const (
    Genre_GENRE_UNSPECIFIED   Genre = 0
    Genre_GENRE_ROCK          Genre = 1
    Genre_GENRE_INDIE         Genre = 2
    Genre_GENRE_DRUM_AND_BASS Genre = 3
)

protobuf 編譯器還會產生從整數值到字串名稱的 map,以及從名稱到值的 map

var Genre_name = map[int32]string{
    0: "GENRE_UNSPECIFIED",
    1: "GENRE_ROCK",
    2: "GENRE_INDIE",
    3: "GENRE_DRUM_AND_BASS",
}
var Genre_value = map[string]int32{
    "GENRE_UNSPECIFIED":   0,
    "GENRE_ROCK":          1,
    "GENRE_INDIE":         2,
    "GENRE_DRUM_AND_BASS": 3,
}

請注意,.proto 語言允許讓多個列舉符號具有相同的數值。具有相同數值的符號是同義詞。這些在 Go 中以完全相同的方式表示,多個名稱對應於相同的數值。反向對應包含單個項目,從數值對應到 .proto 檔案中首先出現的名稱。

擴充 (proto2)

給定一個擴充定義

extend Concert {
  optional int32 promo_id = 123;
}

protocol buffer 編譯器將產生一個名為 E_Promo_idprotoreflect.ExtensionType 值。此值可以與 proto.GetExtensionproto.SetExtensionproto.HasExtensionproto.ClearExtension 函數一起使用,以存取訊息中的擴充。GetExtension 函數和 SetExtension 函數分別傳回和接受包含擴充值類型的 interface{} 值。

對於單數純量擴充欄位,擴充值類型是 純量值類型表 中的對應 Go 類型。

對於單數嵌入訊息擴充欄位,擴充值類型為 *M,其中 M 是欄位訊息類型。

對於重複擴充欄位,擴充值類型是單數類型的切片。

例如,給定以下定義

extend Concert {
  optional int32 singular_int32 = 1;
  repeated bytes repeated_strings = 2;
  optional Band singular_message = 3;
}

擴充值可以按如下方式存取

m := &somepb.Concert{}
proto.SetExtension(m, extpb.E_SingularInt32, int32(1))
proto.SetExtension(m, extpb.E_RepeatedString, []string{"a", "b", "c"})
proto.SetExtension(m, extpb.E_SingularMessage, &extpb.Band{})

v1 := proto.GetExtension(m, extpb.E_SingularInt32).(int32)
v2 := proto.GetExtension(m, extpb.E_RepeatedString).([][]byte)
v3 := proto.GetExtension(m, extpb.E_SingularMessage).(*extpb.Band)

擴充可以在另一種類型內部巢狀宣告。例如,常見的模式是執行類似以下操作

message Promo {
  extend Concert {
    optional int32 promo_id = 124;
  }
}

在這種情況下,ExtensionType 值名為 E_Promo_Concert

服務

Go 程式碼產生器預設不會為服務產生輸出。如果您啟用 gRPC 外掛程式 (請參閱 gRPC Go 快速入門指南),則會產生程式碼以支援 gRPC。