Go 生成程式碼指南 (不透明)

描述 protocol buffer 編譯器為任何給定的 protocol 定義所生成的 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 編譯器才能找到它。

Protocol Buffer 編譯器在呼叫 go_out 標誌時會生成 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 目錄。編譯器會自動建立必要的巢狀輸出子目錄,但不會建立輸出目錄本身。

包(Packages)

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

  • .proto 檔案中宣告,或者
  • 在呼叫 protoc 時在命令列上宣告。

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

Go 匯入路徑在 .proto 檔案中本地指定,方法是宣告一個 go_package 選項,其中包含 Go 包的完整匯入路徑。示例用法:

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

API 級別

生成的程式碼使用 Open Struct API 或 Opaque API。有關介紹,請參閱 Go Protobuf:新的不透明 API 部落格文章。

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

.proto 語法API 級別
proto2Open Struct API
proto3Open Struct API
edition 2023Open Struct API
edition 2024+Opaque API

您可以透過在 .proto 檔案中設定 api_level editions 功能來選擇 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 級別,則需要先將該檔案遷移到 editions。

訊息

給定一個簡單的訊息宣告:

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 編譯器為訊息中定義的每個欄位生成訪問器方法 (setter 和 getter)。

請注意,生成的 Go 訪問器方法始終使用駝峰式命名,即使 .proto 檔案中的欄位名使用下劃線分隔的小寫字母 (這應該是這樣的)。大小寫轉換工作如下:

  1. 第一個字母大寫以匯出。如果第一個字元是下劃線,則將其刪除並在前面加上大寫的 X。
  2. 如果內部下劃線後跟一個小寫字母,則刪除下劃線,並將後面的字母大寫。

因此,您可以使用 Go 中的 GetBirthYear() 方法訪問 proto 欄位 birth_year,並使用 GetXBirthYear_2() 訪問 _birth_year_2

單一欄位

對於此欄位定義:

// proto2 and proto3
message Artist {
  optional int32 birth_year = 1;
}

// editions
message Artist {
  int32 birth_year = 1 [features.field_presence = EXPLICIT];
}

編譯器將生成一個 Go 結構體,其中包含以下訪問器方法:

func (m *Artist) GetBirthYear() int32
func (m *Artist) SetBirthYear(v int32)

對於隱式存在,getter 返回 birth_year 中的 int32 值,或者如果欄位未設定,則返回該型別的 零值 (數字為 0,字串為空字串)。對於顯式存在,getter 返回 birth_year 中的 int32 值,或者如果欄位未設定,則返回預設值。如果未顯式設定預設值,則使用零值。

對於其他標量欄位型別 (包括 boolbytesstring),int32 將根據 標量值型別表 替換為相應的 Go 型別。

在具有顯式存在的欄位中,您還可以使用這些方法:

func (m *Artist) HasBirthYear() bool
func (m *Artist) ClearBirthYear()

奇異訊息欄位

給定訊息型別:

message Band {}

對於具有 Band 欄位的訊息:

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

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

編譯器將生成一個 Go 結構體,其中包含以下訪問器方法:

type Concert struct { ... }

func (m *Concert) GetHeadliner() *Band { ... }
func (m *Concert) SetHeadliner(v *Band) { ... }
func (m *Concert) HasHeadliner() bool { ... }
func (m *Concert) ClearHeadliner() { ... }

即使 m 為 nil,呼叫 GetHeadliner() 訪問器方法也是安全的。這使得無需中間 nil 檢查即可鏈式呼叫 get 操作:

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

如果欄位未設定,getter 將返回欄位的預設值。對於訊息,預設值是指向 nil 的指標。

與 getter 相反,setter 不會自動執行 nil 檢查。因此,您不能安全地在可能為 nil 的訊息上呼叫 setter。

重複欄位

對於重複欄位,訪問器方法使用切片型別。對於此帶有重複欄位的訊息:

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

編譯器將生成一個 Go 結構體,其中包含以下訪問器方法:

type Concert struct { ... }

func (m *Concert) GetSupportActs() []*Band { ... }
func (m *Concert) SetSupportActs(v []*Band) { ... }

同樣,對於欄位定義 repeated bytes band_promo_images = 1;,編譯器將生成處理 [][]byte 型別的訪問器。對於重複的 列舉 repeated MusicGenre genres = 2;,編譯器將生成處理 []MusicGenre 型別的訪問器。

以下示例展示瞭如何使用 構建器 來構造一個 Concert 訊息。

concert := Concert_builder{
  SupportActs: []*Band{
    {}, // First element.
    {}, // Second element.
  },
}.Build()

或者,您也可以使用 setter:

concert := &Concert{}
concert.SetSupportActs([]*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[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 { ... }

func (m *MerchBooth) GetItems() map[string]*MerchItem { ... }
func (m *MerchBooth) SetItems(v map[string]*MerchItem) { ... }

Oneof 欄位

對於 oneof 欄位,protobuf 編譯器為 oneof 中的每個 單一欄位 生成訪問器。

對於此帶有 oneof 欄位的訊息:

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

編譯器將生成一個 Go 結構體,其中包含以下訪問器方法:

type Profile struct { ... }

func (m *Profile) WhichAvatar() case_Profile_Avatar { ... }
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) HasAvatar() bool { ... }
func (m *Profile) HasImageUrl() bool { ... }
func (m *Profile) HasImageData() bool { ... }

func (m *Profile) ClearAvatar() { ... }
func (m *Profile) ClearImageUrl() { ... }
func (m *Profile) ClearImageData() { ... }

以下示例展示瞭如何使用 構建器 設定該欄位:

p1 := accountpb.Profile_builder{
  ImageUrl: proto.String("https://example.com/image.png"),
}.Build()

…或等效地,使用 setter:

// imageData is []byte
imageData := getImageData()
p2 := &accountpb.Profile{}
p2.SetImageData(imageData)

要訪問該欄位,您可以使用 switch 語句根據 WhichAvatar() 的結果進行判斷:

switch m.WhichAvatar() {
case accountpb.Profile_ImageUrl_case:
    // Load profile image based on URL
    // using m.GetImageUrl()

case accountpb.Profile_ImageData_case:
    // Load profile image based on bytes
    // using m.GetImageData()

case accountpb.Profile_Avatar_not_set_case:
    // The field is not set.

default:
    return fmt.Errorf("Profile.Avatar has an unexpected new oneof field %v", x)
}

構建器

構建器是一種方便的方式,可以在單個表示式中構造和初始化訊息,特別是在處理巢狀訊息 (如單元測試) 時。

與其他語言 (如 Java) 中的構建器不同,Go protobuf 構建器不適合在函式之間傳遞。而是立即呼叫 Build() 並傳遞生成的訊息,然後使用 setter 來修改欄位。

以下是使用構建器建立 Band 訊息的示例,該訊息是封閉的 Concert 訊息中的唯一重複欄位:

concert := Concert_builder{
  SupportActs: []*Band{
    Band_builder{
      Name: proto.String("Varint and the Marshals"),
    }.Build()
  },
}.Build()

列舉

給定一個列舉,如下所示:

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
)

Protocol Buffer 編譯器還生成一個從整數值到字串名稱的對映,以及一個從名稱到值的對映:

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

服務(Services)

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