Go 生成程式碼指南 (Open)

描述 protocol buffer 編譯器為任何給定的 protocol 定義所生成的 Go 程式碼。

proto2、proto3 和 editions 生成程式碼之間的任何差異都會被突出顯示 - 請注意,這些差異存在於本文件所述的生成程式碼中,而不是基礎 API,基礎 API 在兩個版本中都是相同的。在閱讀本文件之前,您應該閱讀 proto2 語言指南proto3 語言指南editions 語言指南

編譯器呼叫

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 包的匯入路徑。有兩種方法可以指定 Go 匯入路徑:

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

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

Go 匯入路徑在 .proto 檔案中是本地指定的,透過宣告一個 go_package 選項,並指定 Go 包的完整匯入路徑。例如:

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: The new Opaque 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 編譯器為訊息中定義的每個欄位生成一個結構體欄位。此欄位的確切性質取決於其型別以及它是 singular、repeated、map 還是 oneof 欄位。

請注意,生成的 Go 欄位名始終使用駝峰式命名,即使 .proto 檔案中的欄位名使用小寫帶下劃線(應如此)。大小寫轉換如下:

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

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

Singular Explicit Presence Scalar Fields

對於欄位定義:

int32 birth_year = 1;

編譯器將生成一個具有 *int32 欄位 BirthYear 的結構體,以及一個訪問器方法 GetBirthYear(),該方法返回 Artist 中的 int32 值,如果欄位未設定,則返回預設值。如果未顯式設定預設值,則使用該型別的 零值(數字為 0,字串為空字串)。

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

Singular Implicit Presence Scalar Fields

對於此欄位定義:

int32 birth_year = 1;

編譯器將生成一個具有 int32 欄位 BirthYear 的結構體,以及一個訪問器方法 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;
}

// editions
message Concer {
  Band headliner = 1;
}

編譯器將生成一個 Go 結構體:

type Concert struct {
    Headliner *Band
}

訊息欄位可以設定為 nil,這意味著該欄位未設定,有效地清除了該欄位。這不等同於將值設定為訊息結構體的“空”例項。

編譯器還生成一個 func (m *Concert) GetHeadliner() *Band 輔助函式。如果 m 為 nil 或 headliner 未設定,該函式將返回 nil *Band。這使得可以在沒有中間 nil 檢查的情況下鏈式呼叫 get 方法。

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

重複欄位

每個 repeated 欄位會在 Go 中的結構體中生成一個 T 型別的欄位,其中 T 是欄位的元素型別。對於帶有 repeated 欄位的訊息:

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;,編譯器將生成一個具有 [][]byte 欄位 BandPromoImage 的 Go 結構體。對於 repeated 列舉,如 repeated MusicGenre genres = 2;,編譯器將生成一個具有 []MusicGenre 欄位 Genre 的結構體。

以下示例展示瞭如何設定欄位:

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[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 中的每個 singular 欄位生成一個結構體。這些都實現了 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 編譯器還生成一個從整數值到字串名稱的對映,以及一個從名稱到值的對映:

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 檔案中。

擴充套件

給定一個擴充套件定義:

extend Concert {
  int32 promo_id = 123;
}

Protocol buffer 編譯器將生成一個 protoreflect.ExtensionType 值,名為 E_Promo_id。此值可與 proto.GetExtensionproto.SetExtensionproto.HasExtensionproto.ClearExtension 函式一起使用,以訪問訊息中的擴充套件。GetExtension 函式和 SetExtension 函式分別返回和接受一個包含擴充套件值型別的 interface{} 值。

對於 singular 標量擴充套件欄位,擴充套件值型別是 標量值型別表 中相應的 Go 型別。

對於 singular 嵌入式訊息擴充套件欄位,擴充套件值型別是 *M,其中 M 是欄位訊息型別。

對於 repeated 擴充套件欄位,擴充套件值型別是 singular 型別的切片。

例如,給定以下定義:

extend Concert {
  int32 singular_int32 = 1;
  repeated bytes repeated_strings = 2;
  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 {
    int32 promo_id = 124;
  }
}

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

服務(Services)

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