Protobuf 生成 Go 程式碼指南

KevinYan發表於2019-12-05

這個教程中將會描述protocol buffer編譯器透過給定的.proto會編譯生成什麼Go程式碼。教程針對的是proto3版本的protobuf。在閱讀之前確保你已經閱讀過Protobuf語言指南

編譯器呼叫

Protobuf核心的工具集是C++語言開發的,官方的protoc編譯器中並不支援Go語言,需要安裝一個外掛才能生成Go程式碼。用如下命令安裝:

$ go get github.com/golang/protobuf/protoc-gen-go

提供了一個protoc-gen-go二進位制檔案,當編譯器呼叫時傳遞了--go_out命令列標誌時protoc就會使用它。--go_out告訴編譯器把Go原始碼寫到哪裡。編譯器會為每個.proto檔案生成一個單獨的原始碼檔案。

輸出檔案的名稱是透過獲取.proto檔案的名稱並進行兩處更改來計算的:

  • 生成檔案的副檔名是.pb.go。比如說player_record.proto編譯後會得到player_record.pb.go
  • proto路徑(使用--proto_path-I命令列標誌指定)將替換為輸出路徑(使用--go_out標誌指定)。

當你執行如下編譯命令時:

protoc --proto_path=src --go_out=build/gen src/foo.proto src/bar/baz.proto

編譯器會讀取檔案src/foo.protosrc/bar/baz.proto,這將會生成兩個輸出檔案build/gen/foo.pb.gobuild/gen/bar/baz.pb.go

如果有必要,編譯器會自動生成build/gen/bar目錄,但是他不能建立build或者build/gen目錄,這兩個必須是已經存在的目錄。

如果一個.proto檔案中有包宣告,生成的原始碼將會使用它來作為Go的包名,如果.proto的包名中有. 在Go包名中會將.轉換為_。舉例來說proto包名example.high_score將會生成Go包名example_high_score

.proto檔案中可以使用option go_package指令來覆蓋上面預設生成Go包名的規則。比如說包含如下指令的一個.proto檔案

package example.high_score;
option go_package = "hs";

生成的Go原始碼的包名是hs

如果一個.proto檔案中不包含package宣告,生成的原始碼將會使用.proto檔案的檔名(去掉副檔名)作為Go包名,.會被首先轉換為_。舉例來說一個名為high.score.proto不包含pack宣告的檔案將會生成檔案high.score.pb.go,他的Go包名是high_score

訊息

一個簡單的訊息宣告:

message Foo {}

protocol buffer編譯器將會生成一個名為Foo的結構體,實現了proto.Message介面的Foo型別的指標

type Foo struct {
}

// 重置proto為預設值
func (m *Foo) Reset()         { *m = Foo{} }

// String 返回proto的字串表示
func (m *Foo) String() string { return proto.CompactTextString(m) }

// ProtoMessage作為一個tag 確保其他人不會意外的實現
// proto.Message 介面.
func (*Foo) ProtoMessage()    {}

內嵌的訊息

一個message可以宣告在其他message的內部。比如說:

message Foo {
  message Bar {
  }
}

這種情況,編譯器會生成兩個結構體:FooFoo_Bar

預定義訊息型別

Protobufs帶有一組預定義的訊息,稱為眾所周知的型別(WKT)。這些型別可以用於與其他服務的互操作性,或者僅僅因為它們簡潔地表示了常見的有用模式。例如,Struct訊息表示任意C樣式結構的格式。

WKT的預生成Go程式碼作為Go protobuf庫的一部分進行分發,如果message中使用了WKT,則生成的訊息的Go程式碼會引用此程式碼。例如,給出如下訊息:

import "google/protobuf/struct.proto"
import "google/protobuf/timestamp.proto"

message NamedStruct {
  string name = 1;
  google.protobuf.Struct definition = 2;
  google.protobuf.Timestamp last_modified = 3;
}

生成的Go程式碼將會像下面這樣:

import google_protobuf "github.com/golang/protobuf/ptypes/struct"
import google_protobuf1 "github.com/golang/protobuf/ptypes/timestamp"

...

type NamedStruct struct {
   Name         string
   Definition   *google_protobuf.Struct
   LastModified *google_protobuf1.Timestamp
}

一般來說,您不需要將這些型別直接匯入程式碼中。但是,如果需要直接引用其中一種型別,只需匯入github.com/golang/protobuf/ptypes/[TYPE]包,並正常使用該型別。

欄位

編譯器會為每個在message中定義的欄位生成一個Go結構體的欄位,欄位的確切性質取決於它的型別以及它是singularrepeatedmap還是oneof欄位。

注意生成的Go結構體的欄位將始終使用駝峰命名,即使在.proto檔案中訊息欄位用的是小寫加下劃線(應該這樣)。大小寫轉換的原理如下:

  • 首字母會大些,如果message中欄位的第一個字元是_,它將被替換為X。
  • 如果內部下劃線後跟小寫字母,則刪除下劃線,並將後面跟隨的字母大寫。

因此,proto欄位foo_bar_baz在Go中變成FooBarBaz_my_field_name_2變為XMyFieldName_2

單一標量欄位

對於欄位定義:

int32 foo = 1;

編譯器將生成一個帶有名為Foo的int32欄位和一個訪問器方法GetFoo()的結構,該方法返回Foo中的int32值或該欄位的零值(如果欄位未設定(數值型零值為0,字串為空字串))。

單一message欄位

給出如下訊息型別

message Bar {}

對於一個有Bar型別欄位的訊息:

// proto3
message Baz {
  Bar foo = 1;
}

編譯器將會生成一個Go結構體

type Baz struct {
        Foo *Bar
}

訊息型別的欄位可以設定為nil,這意味著該欄位未設定,有效清除該欄位。這不等同於將值設定為訊息結構體的“空”例項。

編譯器還生成一個func(m * Baz)GetFoo()* Bar輔助函式。這讓不在中間檢查nil值進行鏈式呼叫成為可能。

可重複欄位

每個重複的欄位在Go中的結構中生成一個T型別的slice,其中T是欄位的元素型別。對於帶有重複欄位的此訊息:

message Baz {
  repeated Bar foo = 1;
}

編譯器會生成如下結構體:

type Baz struct {
        Foo  []*Bar
}

同樣,對於欄位定義repeated bytes foo = 1;編譯器將會生成一個帶有型別為[][]byte名為Foo的欄位的Go結構體。對於可重複的列舉repeated MyEnum bar = 2;,編譯器會生成帶有型別為[]MyEnum名為Bar的欄位的Go結構體。

對映欄位

每個對映欄位會在Go的結構體中生成一個map[TKey]TValue型別的欄位,其中TKey是欄位的鍵型別TValue是欄位的值型別。對於下面這個訊息定義:

message Bar {}

message Baz {
  map<string, Bar> foo = 1;
}

編譯器生成Go結構體

type Baz struct {
        Foo map[string]*Bar
}

列舉

給出如下列舉

message SearchRequest {
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 1;
  ...
}

編譯器將會生成一個列舉型別和一系列該型別的常量。

對於訊息中的列舉(像上面那樣),型別名字以訊息名開頭

type SearchRequest_Corpus int32

對於包級別的列舉:

// .proto
enum Foo {
  DEFAULT_BAR = 0;
  BAR_BELLS = 1;
  BAR_B_CUE = 2;
}

Go 中的型別不會對proto中的列舉名稱進行修改:

type Foo int32

此型別具有String()方法,該方法返回給定值的名稱。

Enum()方法使用給定值初始化新分配的記憶體並返回相應的指標:

func (Foo) Enum() *Foo

編譯器為列舉中的每個值生成一個常量。對於訊息中的列舉,常量以訊息的名稱開頭:

const (
        SearchRequest_UNIVERSAL SearchRequest_Corpus = 0
        SearchRequest_WEB       SearchRequest_Corpus = 1
        SearchRequest_IMAGES    SearchRequest_Corpus = 2
        SearchRequest_LOCAL     SearchRequest_Corpus = 3
        SearchRequest_NEWS      SearchRequest_Corpus = 4
        SearchRequest_PRODUCTS  SearchRequest_Corpus = 5
        SearchRequest_VIDEO     SearchRequest_Corpus = 6
)

對於包級別的列舉,常量以列舉名稱開頭:

const (
        Foo_DEFAULT_BAR Foo = 0
        Foo_BAR_BELLS   Foo = 1
        Foo_BAR_B_CUE   Foo = 2
)

protobuf編譯器還生成從整數值到字串名稱的對映以及從名稱到值的對映:

var Foo_name = map[int32]string{
        0: "DEFAULT_BAR",
        1: "BAR_BELLS",
        2: "BAR_B_CUE",
}
var Foo_value = map[string]int32{
        "DEFAULT_BAR": 0,
        "BAR_BELLS":   1,
        "BAR_B_CUE":   2,
}

請注意,.proto語言允許多個列舉符號具有相同的數值。具有相同數值的符號是同義詞。這些在Go中以完全相同的方式表示,多個名稱對應於相同的數值。反向對映包含數字值的單個條目,數值對映到出現在proto檔案中首先出現的名稱。

服務

預設情況下,Go程式碼生成器不會為服務生成輸出。如果您啟用gRPC外掛(請參閱gRPC Go快速入門指南),則會生成程式碼以支援gRPC。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
公眾號:網管叨bi叨 | Golang、Laravel、Docker、K8s等學習經驗分享

相關文章