Go 程式碼產生指南(不透明)
proto2 和 proto3 產生的程式碼之間的任何差異都會被突顯出來 - 請注意,這些差異在於本文檔中描述的產生的程式碼,而不是基礎 API,基礎 API 在兩個版本中是相同的。在閱讀本文檔之前,您應該閱讀 proto2 語言指南 和/或 proto3 語言指南。
注意
您正在查看不透明 API 的文件,這是目前版本。如果您正在使用使用較舊的開放結構 API 的 .proto 檔案(您可以從各自的 .proto 檔案中的 API 層級設定判斷),請參閱 Go 程式碼產生(開放) 以取得相關文件。請參閱 Go Protobuf:新的不透明 API 以了解不透明 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 編譯器才能找到它。
當使用 go_out
標記調用時,protocol buffer 編譯器會產生 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
目錄。編譯器會在必要時自動建立巢狀輸出子目錄,但不會建立輸出目錄本身。
套件
為了產生 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";
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 層級
產生的程式碼可以使用開放結構 API 或不透明 API。請參閱 Go Protobuf:新的不透明 API 部落格文章以取得簡介。
根據您的 .proto
檔案使用的語法,以下將使用哪個 API
.proto 語法 | API 層級 |
---|---|
proto2 | 開放結構 API |
proto3 | 開放結構 API |
版本 2023 | 開放結構 API |
版本 2024+ | 不透明 API |
您可以透過在 .proto
檔案中設定 api_level
版本功能來選擇 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 層級,您需要先將該檔案遷移到版本。
訊息
給定一個簡單的訊息宣告
message Artist {}
protocol buffer 編譯器會產生一個名為 Artist
的 struct。*Artist
實作 proto.Message
介面。
proto
套件 提供了對訊息進行操作的函數,包括與二進制格式之間的轉換。
proto.Message
介面定義了一個 ProtoReflect
方法。此方法傳回一個 protoreflect.Message
,它提供訊息的基於反射的視圖。
optimize_for
選項不會影響 Go 程式碼產生器的輸出。
當多個 goroutine 並行存取同一個訊息時,以下規則適用
- 並行存取(讀取)欄位是安全的,但有一個例外
- 第一次存取 lazy 欄位 是一種修改。
- 修改同一個訊息中的不同欄位是安全的。
- 並行修改欄位是不安全的。
- 以任何方式並行修改訊息與
proto
套件 的函數(例如proto.Marshal
或proto.Size
)是不安全的。
巢狀類型
訊息可以在另一個訊息內部宣告。例如
message Artist {
message Name {
}
}
在這種情況下,編譯器會產生兩個 struct:Artist
和 Artist_Name
。
欄位
protocol buffer 編譯器會為訊息中定義的每個欄位產生存取器方法(setter 和 getter)。
請注意,產生的 Go 存取器方法始終使用駝峰式命名,即使 .proto
檔案中的欄位名稱使用帶底線的小寫字母(應如此)。大小寫轉換的工作方式如下
- 第一個字母會大寫以供匯出。如果第一個字元是底線,則會移除它並在前面加上大寫 X。
- 如果內部底線後面跟著小寫字母,則會移除底線,並將後面的字母大寫。
因此,您可以使用 Go 中的 GetBirthYear()
方法存取 proto 欄位 birth_year
,並使用 GetXBirthYear_2()
存取 _birth_year_2
。
單數純量欄位(proto2)
對於這些欄位定義中的任何一個
optional int32 birth_year = 1;
required int32 birth_year = 1;
編譯器會產生以下存取器方法
func (m *Artist) GetBirthYear() int32 { ... }
func (m *Artist) SetBirthYear(v int32) { ... }
func (m *Artist) HasBirthYear() bool { ... }
func (m *Artist) ClearBirthYear() { ... }
存取器方法 GetBirthYear()
會傳回 birth_year
中的 int32
值,如果欄位未設定,則傳回預設值。如果未明確設定預設值,則會改為使用該類型的零值(數字為 0
,字串為空字串)。
對於其他純量欄位類型(包括 bool
、bytes
和 string
),int32
會根據純量值類型表替換為對應的 Go 類型。
單數純量欄位(proto3)
對於此欄位定義
int32 birth_year = 1;
optional int32 first_active_year = 2;
編譯器會產生以下存取器方法
func (m *Artist) GetBirthYear() int32 { ... }
func (m *Artist) SetBirthYear(v int32) { ... }
// NOTE: No HasBirthYear() or ClearBirthYear() methods;
// proto3 fields only have presence when declared as optional:
// /programming-guides/field_presence.md
func (m *Artist) GetFirstActiveYear() int32 { ... }
func (m *Artist) SetFirstActiveYear(v int32) { ... }
func (m *Artist) HasFirstActiveYear() bool { ... }
func (m *Artist) ClearFirstActiveYear() { ... }
存取器方法 GetBirthYear()
會傳回 birth_year
中的 int32
值,如果欄位未設定,則傳回該類型的 零值(數字為 0
,字串為空字串)。
對於其他純量欄位類型(包括 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;
}
編譯器將產生一個具有以下存取器方法的 Go struct
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()
存取器方法。這使得可以鏈式調用 get 而無需中間 nil 檢查
var m *Concert // defaults to nil
log.Infof("GetFoundingYear() = %d (no panic!)", m.GetHeadliner().GetFoundingYear())
如果欄位未設定,getter 將傳回欄位的預設值。對於訊息,預設值為 nil 指標。
與 getter 相反,setter 不會為您執行 nil 檢查。因此,您不能安全地在可能為 nil 的訊息上調用 setter。
重複欄位
對於重複欄位,存取器方法使用 slice 類型。對於具有重複欄位的此訊息
message Concert {
// Best practice: use pluralized names for repeated fields:
// /programming-guides/style#repeated-fields
repeated Band support_acts = 1;
}
編譯器會產生一個具有以下存取器方法的 Go struct
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 欄位都會產生使用類型 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 struct
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 struct
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()
並傳遞產生的 proto 訊息,然後使用 setter 修改欄位。
列舉
給定一個類似以下的列舉
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 編譯器還會產生一個從整數值到字串名稱的 map 和一個從名稱到值的 map
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
是欄位訊息類型。
對於重複擴充欄位,擴充值類型是單數類型的 slice。
例如,給定以下定義
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。