Go 生成程式碼指南 (不透明)
proto2 和 proto3 生成程式碼之間的任何差異都會被突出顯示——請注意,這些差異存在於本文件所述的生成程式碼中,而不是基本 API,後者在兩個版本中是相同的。在閱讀本文件之前,您應該閱讀 proto2 語言指南 和/或 proto3 語言指南。
注意
您正在檢視不透明 API 的文件,這是當前版本。如果您正在處理使用舊的 Open Struct API 的 .proto 檔案 (可以透過相應 .proto 檔案中的 API 級別設定來判斷),請參閱 Go 生成程式碼 (開放) 獲取相應的文件。有關不透明 API 的介紹,請參閱 Go Protobuf:新的不透明 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 包的匯入路徑 (包括被生成 .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 級別 |
|---|---|
| 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 編譯器為訊息中定義的每個欄位生成訪問器方法 (setter 和 getter)。
請注意,生成的 Go 訪問器方法始終使用駝峰式命名,即使 .proto 檔案中的欄位名使用下劃線分隔的小寫字母 (這應該是這樣的)。大小寫轉換工作如下:
- 第一個字母大寫以匯出。如果第一個字元是下劃線,則將其刪除並在前面加上大寫的 X。
- 如果內部下劃線後跟一個小寫字母,則刪除下劃線,並將後面的字母大寫。
因此,您可以使用 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 值,或者如果欄位未設定,則返回預設值。如果未顯式設定預設值,則使用零值。
對於其他標量欄位型別 (包括 bool、bytes 和 string),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_id 的 protoreflect.ExtensionType 值。此值可與 proto.GetExtension、proto.SetExtension、proto.HasExtension 和 proto.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。