Go 程式碼產生指南 (Open)
所有 proto2 和 proto3 產生程式碼之間的差異都會特別標示出來 - 請注意,這些差異在於本文件中描述的產生程式碼,而不是基礎 API,兩者在兩個版本中是相同的。在閱讀本文檔之前,您應該先閱讀 proto2 語言指南 和/或 proto3 語言指南。
注意
您正在查看舊版產生程式碼 API (Open Struct API) 的文件。請參閱 Go 產生程式碼 (Opaque) 以取得 (新) Opaque API 的對應文件。請參閱 Go Protobuf:新的 Opaque API 以取得 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 編譯器才能找到它。
當使用 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
檔案 (包括那些被產生的 .proto
檔案以遞移方式依賴的檔案) 提供 Go 套件的匯入路徑。有兩種方法可以指定 Go 匯入路徑
- 在
.proto
檔案中宣告,或 - 在調用
protoc
時在命令列上宣告。
我們建議在 .proto
檔案中宣告,以便 .proto
檔案的 Go 套件可以集中識別 .proto
檔案本身,並簡化調用 protoc
時傳遞的旗標集。如果給定 .proto
檔案的 Go 匯入路徑由 .proto
檔案本身和命令列提供,則後者優先於前者。
Go 匯入路徑在 .proto
檔案中以本機方式指定,方法是宣告具有 Go 套件完整匯入路徑的 go_package
選項。範例用法
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:新的 Opaque API 部落格文章以取得介紹。
根據您的 .proto
檔案使用的語法,以下是將使用的 API
.proto 語法 | API 層級 |
---|---|
proto2 | Open Struct API |
proto3 | Open Struct API |
版本 2023 | Open Struct API |
版本 2024+ | Opaque 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
的結構。*Artist
實作了 proto.Message
介面。
proto
套件 提供了對訊息進行操作的函數,包括與二進位格式之間的轉換。
proto.Message
介面定義了一個 ProtoReflect
方法。此方法會傳回一個 protoreflect.Message
,它提供了訊息的反射式檢視。
optimize_for
選項不會影響 Go 程式碼產生器的輸出。
當多個 goroutine 同時存取同一個訊息時,適用以下規則
- 同時存取 (讀取) 欄位是安全的,但有一個例外
- 第一次存取 lazy 欄位 是一種修改。
- 修改同一個訊息中的不同欄位是安全的。
- 同時修改一個欄位是不安全的。
- 以任何方式同時修改訊息與
proto
套件 的函數 (例如proto.Marshal
或proto.Size
) 是不安全的。
巢狀類型
訊息可以在另一個訊息內部宣告。例如
message Artist {
message Name {
}
}
在這種情況下,編譯器會產生兩個結構:Artist
和 Artist_Name
。
欄位
protocol buffer 編譯器會為訊息中定義的每個欄位產生一個結構欄位。此欄位的確切性質取決於其類型以及它是單數、重複、map 或 oneof 欄位。
請注意,產生的 Go 欄位名稱始終使用駝峰式命名,即使 .proto
檔案中的欄位名稱使用帶底線的小寫 (應該如此)。大小寫轉換的工作方式如下
- 第一個字母會大寫以進行匯出。如果第一個字元是底線,則會移除它並在前面加上大寫 X。
- 如果內部底線後跟小寫字母,則會移除底線,並將後面的字母大寫。
因此,proto 欄位 birth_year
在 Go 中變成 BirthYear
,而 _birth_year_2
變成 XBirthYear_2
。
單數純量欄位 (proto2)
對於以下任一欄位定義
optional int32 birth_year = 1;
required int32 birth_year = 1;
編譯器會產生一個結構,其中包含名為 BirthYear
的 *int32
欄位和一個存取器方法 GetBirthYear()
,該方法傳回 Artist
中的 int32
值,如果欄位未設定,則傳回預設值。如果未明確設定預設值,則會改用該類型的 零值 (數字為 0
,字串為空字串)。
對於其他純量欄位類型 (包括 bool
、bytes
和 string
),*int32
會根據 純量值類型表 替換為對應的 Go 類型。
單數純量欄位 (proto3)
對於此欄位定義
int32 birth_year = 1;
optional int32 first_active_year = 2;
編譯器將產生一個結構,其中包含名為 BirthYear
的 int32
欄位和一個存取器方法 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;
}
編譯器將產生一個 Go 結構
type Concert struct {
Headliner *Band
}
訊息欄位可以設定為 nil
,這表示欄位未設定,有效地清除欄位。這不等同於將值設定為訊息結構的「空」實例。
編譯器還會產生一個 func (m *Concert) GetHeadliner() *Band
輔助函數。如果 m
為 nil 或 headliner
未設定,此函數會傳回 nil
*Band
。這使得可以鏈式呼叫 get,而無需中間 nil
檢查
var m *Concert // defaults to nil
log.Infof("GetFoundingYear() = %d (no panic!)", m.GetHeadliner().GetFoundingYear())
重複欄位
每個重複欄位都會在 Go 的結構中產生一個 T
欄位的切片,其中 T
是欄位的元素類型。對於具有重複欄位的此訊息
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;
,編譯器將產生一個 Go 結構,其中包含名為 BandPromoImage
的 [][]byte
欄位。對於重複的 列舉,例如 repeated MusicGenre genres = 2;
,編譯器會產生一個結構,其中包含名為 Genre
的 []MusicGenre
欄位。
以下範例顯示如何設定欄位
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 欄位都會在結構中產生一個 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 中的每個 單數欄位 產生一個結構。這些結構都實作了此 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 編譯器還會產生從整數值到字串名稱的 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
是欄位訊息類型。
對於重複擴充欄位,擴充值類型是單數類型的切片。
例如,給定以下定義
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。