Go 程式碼產生指南
proto2 和 proto3 產生程式碼之間的任何差異都會被強調 - 請注意,這些差異在本文件中描述的產生程式碼中,而不是基本 API 中,基本 API 在兩個版本中是相同的。在閱讀本文件之前,您應該閱讀 proto2 語言指南和/或 proto3 語言指南。
編譯器調用
協定緩衝區編譯器需要一個外掛程式才能產生 Go 程式碼。使用 Go 1.16 或更高版本執行下列命令進行安裝
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
這會在 $GOBIN
中安裝 protoc-gen-go
二進位檔。設定 $GOBIN
環境變數以變更安裝位置。它必須在您的 $PATH
中,編譯器才能找到它。
當使用 go_out
旗標調用時,協定緩衝區編譯器會產生 Go 輸出。 go_out
旗標的參數是您希望編譯器寫入 Go 輸出的目錄。 編譯器會為每個 .proto
檔案輸入建立單一原始程式碼檔案。輸出檔案的名稱是將 .proto
副檔名取代為 .pb.go
而建立。
產生的 .pb.go
檔案放置在輸出目錄中的位置取決於編譯器旗標。有多種輸出模式
- 如果指定
paths=import
旗標,則輸出檔案會放置在以 Go 套件的匯入路徑命名的目錄中(例如.proto
檔案中go_package
選項提供的路徑)。例如,Go 匯入路徑為example.com/project/protos/fizz
的輸入檔案protos/buzz.proto
會產生example.com/project/protos/fizz/buzz.pb.go
的輸出檔案。如果未指定paths
旗標,這是預設的輸出模式。 - 如果指定
module=$PREFIX
旗標,則輸出檔案會放置在以 Go 套件的匯入路徑命名的目錄中(例如.proto
檔案中go_package
選項提供的路徑),但會從輸出檔案名稱中移除指定的目錄前置詞。 例如,Go 匯入路徑為example.com/project/protos/fizz
的輸入檔案protos/buzz.proto
,且將example.com/project
指定為module
前置詞,會產生protos/fizz/buzz.pb.go
的輸出檔案。 在模組路徑之外產生任何 Go 套件都會導致錯誤。 此模式適用於將產生的檔案直接輸出到 Go 模組中。 - 如果指定
paths=source_relative
旗標,則輸出檔案會放置在與輸入檔案相同的相對目錄中。 例如,輸入檔案protos/buzz.proto
會產生protos/buzz.pb.go
的輸出檔案。
當調用 protoc
時,透過傳遞 go_opt
旗標來提供特定於 protoc-gen-go
的旗標。 可以傳遞多個 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
檔案中宣告,以便 Go 套件的 .proto
檔案可以集中識別 .proto
檔案本身,並簡化調用 protoc
時傳遞的旗標集。如果給定 .proto
檔案的 Go 匯入路徑由 .proto
檔案本身和命令列提供,則後者優先於前者。
Go 匯入路徑在 .proto
檔案中透過宣告帶有 Go 套件完整匯入路徑的 go_package
選項進行本地指定。範例用法
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
匯入路徑之間沒有關聯。
訊息
給定一個簡單的訊息宣告
message Artist {}
協定緩衝區編譯器會產生一個名為 Artist
的結構。*Artist
實作 proto.Message
介面。
proto
套件提供對訊息進行操作的函數,包括轉換為二進位格式和從二進位格式轉換。
proto.Message
介面定義了 ProtoReflect
方法。 此方法會傳回 protoreflect.Message
,它提供訊息的基於反射的視圖。
optimize_for
選項不會影響 Go 程式碼產生器的輸出。
巢狀類型
訊息可以在另一個訊息內宣告。例如
message Artist {
message Name {
}
}
在這種情況下,編譯器會產生兩個結構:Artist
和 Artist_Name
。
欄位
協定緩衝區編譯器會為訊息中定義的每個欄位產生一個結構欄位。此欄位的確切性質取決於其類型,以及它是單數、重複、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
,字串為空字串)。
對於其他純量欄位類型(包含 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;
,編譯器將產生一個具有名為 BandPromoImage
的 [][]byte
欄位的 Go 結構。對於重複的列舉,例如 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;
// ...
}
協定緩衝區編譯器會產生一個類型和一系列具有該類型的常數
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
協定緩衝區編譯器會為列舉中的每個值產生一個常數。對於訊息內的列舉,常數會以封閉訊息的名稱開頭
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 檔案中第一個出現的名稱的單一項目。
擴充 (proto2)
給定擴充定義
extend Concert {
optional int32 promo_id = 123;
}
協定緩衝區編譯器將產生一個名為 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。