Go 程式碼產生指南

描述協定緩衝區編譯器為任何給定的協定定義產生確切的 Go 程式碼。

proto2 和 proto3 產生程式碼之間的任何差異都會被強調 - 請注意,這些差異在本文件中描述的產生程式碼中,而不是基本 API 中,基本 API 在兩個版本中是相同的。在閱讀本文件之前,您應該閱讀 proto2 語言指南和/或 proto3 語言指南

編譯器調用

協定緩衝區編譯器需要一個外掛程式才能產生 Go 程式碼。使用 Go 1.16 或更高版本執行下列命令進行安裝

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

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

當使用 go_out 旗標調用時,協定緩衝區編譯器會產生 Go 輸出。 go_out 旗標的參數是您希望編譯器寫入 Go 輸出的目錄。 編譯器會為每個 .proto 檔案輸入建立單一原始程式碼檔案。輸出檔案的名稱是將 .proto 副檔名取代為 .pb.go 而建立。

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

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

當調用 protoc 時,透過傳遞 go_opt 旗標來提供特定於 protoc-gen-go 的旗標。 可以傳遞多個 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 檔案中宣告,以便 Go 套件的 .proto 檔案可以集中識別 .proto 檔案本身,並簡化調用 protoc 時傳遞的旗標集。如果給定 .proto 檔案的 Go 匯入路徑由 .proto 檔案本身和命令列提供,則後者優先於前者。

Go 匯入路徑在 .proto 檔案中透過宣告帶有 Go 套件完整匯入路徑的 go_package 選項進行本地指定。範例用法

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

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

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 匯入路徑之間沒有關聯。

訊息

給定一個簡單的訊息宣告

message Artist {}

協定緩衝區編譯器會產生一個名為 Artist 的結構。*Artist 實作 proto.Message 介面。

proto 套件提供對訊息進行操作的函數,包括轉換為二進位格式和從二進位格式轉換。

proto.Message 介面定義了 ProtoReflect 方法。 此方法會傳回 protoreflect.Message,它提供訊息的基於反射的視圖。

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

巢狀類型

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

message Artist {
  message Name {
  }
}

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

欄位

協定緩衝區編譯器會為訊息中定義的每個欄位產生一個結構欄位。此欄位的確切性質取決於其類型,以及它是單數、重複、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,字串為空字串)。

對於其他純量欄位類型(包含 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;,編譯器將產生一個具有名為 BandPromoImage[][]byte 欄位的 Go 結構。對於重複的列舉,例如 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;
  // ...
}

協定緩衝區編譯器會產生一個類型和一系列具有該類型的常數

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

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

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 編譯器還會產生一個從整數值到字串名稱的映射,以及一個從名稱到值的映射

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;
}

協定緩衝區編譯器將產生一個名為 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。