Go 生成程式碼指南 (Open)
proto2、proto3 和 editions 生成程式碼之間的任何差異都會被突出顯示 - 請注意,這些差異存在於本文件所述的生成程式碼中,而不是基礎 API,基礎 API 在兩個版本中都是相同的。在閱讀本文件之前,您應該閱讀 proto2 語言指南、proto3 語言指南 或 editions 語言指南。
注意
您正在檢視舊的生成程式碼 API(Open Struct API)的文件。有關(新)Opaque API 的相應文件,請參閱 Go 生成程式碼 (Opaque)。有關 Opaque API 的介紹,請參閱 Go Protobuf: The new Opaque API。編譯器呼叫
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.proto 和 bar/baz.proto,並將輸出檔案 foo.pb.go 和 bar/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 級別 |
|---|---|
| proto2 | Open Struct API |
| proto3 | Open Struct API |
| edition 2023 | Open 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.Marshal或proto.Size)不安全。
巢狀型別
訊息可以宣告在另一個訊息內部。例如:
message Artist {
message Name {
}
}
在這種情況下,編譯器會生成兩個結構體:Artist 和 Artist_Name。
欄位
Protocol buffer 編譯器為訊息中定義的每個欄位生成一個結構體欄位。此欄位的確切性質取決於其型別以及它是 singular、repeated、map 還是 oneof 欄位。
請注意,生成的 Go 欄位名始終使用駝峰式命名,即使 .proto 檔案中的欄位名使用小寫帶下劃線(應如此)。大小寫轉換如下:
- 首字母大寫以匯出。如果第一個字元是下劃線,則將其刪除並預先加上大寫 X。
- 如果內部下劃線後跟一個小寫字母,則刪除下劃線,並將後面的字母大寫。
因此,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,字串為空字串)。
對於其他標量欄位型別(包括 bool、bytes 和 string),*int32 將根據 標量值型別表替換為相應的 Go 型別。
Singular Implicit Presence Scalar Fields
對於此欄位定義:
int32 birth_year = 1;
編譯器將生成一個具有 int32 欄位 BirthYear 的結構體,以及一個訪問器方法 GetBirthYear(),該方法返回 birth_year 中的 int32 值,如果欄位未設定,則返回該型別的 零值(數字為 0,字串為空字串)。
FirstActiveYear 結構體欄位的型別將是 *int32,因為它被標記為 optional。
對於其他標量欄位型別(包括 bool、bytes 和 string),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() string 和 func (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.GetExtension、proto.SetExtension、proto.HasExtension 和 proto.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 的程式碼。