この記事について
Protocol Buffers(protobuf) の概要、基本的な使い方、*.proto ファイルの書き方についてざっくりまとめます。
- 検証環境
- OS: Windows 10
- 言語: Go 1.13
Protocol Buffers とは
すでにたくさん情報があるので要点だけ記載。
- データをシリアライズ(マーシャル)するためのフォーマット、メカニズム
- クライアント/サーバー間の通信や、データの永続化などに用いる
- 元々は Google が開発
- IDL でメッセージ(データ構造)を定義
- 様々な言語とプラットフォームで利用可能
- iOS や Android でも利用可能
- バイナリ形式にシリアライズする
- XML よりもサイズが小さくなり、また、高速
- シリアライズ/デシリアライズ をするためのコードが自動生成される
- その他
- 既存のプログラムに影響を与えずにメッセージを更新(フィールドの追加)が可能
バージョン
- 最新バージョンは 3 (proto3と呼ばれる)
- proto3 と、前バージョンの proto2 は、完全には互換性が無い
基本的な使い方
1. Protocol buffers コンパイラ(protoc)をインストール
ダウンロードサイトから自分の環境にあった protoc をダウンロード。今回は v3.11.4 の protoc-3.11.4-win64.zip をダウンロードして展開した。
注意事項:
- 展開した bin フォルダにパスを通すこと
- zip に含まれる include フォルダも展開すること(protoc が参照するファイルが入ってるため)
いちおう、バージョンの確認。
> protoc --version
libprotoc 3.11.4
2. 必要に応じてプラグインをインストール
Go言語の場合は protoc に対するプラグインが必要なのでインストールする。
> go get google.golang.org/protobuf/cmd/protoc-gen-go
> protoc-gen-go --version
protoc-gen-go v1.21.0-devel
なお、ドキュメントには go install
を使うよう書かれてたが、自分の環境ではエラーになったので go get
にした。
※ gRPC のスタブコードの生成についてはProtocol Buffers用 Go言語APIの APIv1 と APIv2 の差異 を参照。
3. *.proto ファイルを作成
*.proto
ファイルでメッセージの定義を行う。
以下は参考用にGo言語用のチュートリアル内の addressbook.proto にコメントを入れたもの。
// Protocol Buffers のバージョンを指定する。省略すると proto2 と見なされる。
syntax = "proto3";
// package は、プロジェクト間での名前衝突を防ぐためのパッケージ名。
// このパッケージ名は各言語に応じた解釈が行われる。
// (例)
// C++ : C++の名前空間になる
// C# : パスカル形式に変換後にC#の名前空間になる(csharp_namespaceの指定が無い場合)
// Go : Goのパッケージ名になる(go_packageの指定が無い場合)
// Python : 無視される
package addresspb;
// 他の *.proto ファイルの定義された型などを読み込みたい場合は import を使う
// 以下の "google/..." は、protoc に含まれる include ディレクトリ配下を指している。
import "google/protobuf/timestamp.proto";
// option は、特定のコンテキストで解釈される。すべてのリストは以下のファイルに記載されてる。
// https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptor.proto
//
// go_package は生成される *.pb.go ファイルのパッケージを指定する。
// 無くてもコンパイルできるが警告が出る。
// なお、セミコロンを付けてパッケージのインポートパスとパッケージ名を別々に指定することも可能。
//
// (例)
// option go_package = "github.com/hoge/fuga"; // セミコロン無し
// option go_package = "github.com/hoge/fuga;fuga"; // セミコロンあり
option go_package = ".;addresspb";
// メッセージ(Person)の定義
message Person {
// メッセージのフィールド。
// 各フィールドの識別子として 1, 2... というフィールド番号(タグ)が必要。
// → シリアライズされたデータでは、フィールド番号でフィールドを識別するため。
string name = 1;
int32 id = 2;
string email = 3;
// 列挙型の定義
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
// メッセージの中に別のメッセージの定義を含められる(定義のネスト)
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
// repeatedは配列(要素の数は任意。0個でもよい。)
repeated PhoneNumber phones = 4;
// Import した *.proto ファイルで定義された型
google.protobuf.Timestamp last_updated = 5;
}
// メッセージ(AddressBook)の定義
// AddressBook は複数の Person を含む。
message AddressBook {
repeated Person people = 1;
}
4. *.proto ファイルをコンパイル
コマンドプロンプトで *.proto
ファイルのあるディレクトリに移動して以下を実行。今回の場合 addressbook.pb.go
が生成される。
protoc addressbook.proto --go_out=.\
5. 生成されたコードを用いてシリアライズ/デシリアライズを行う
生成された *.pb.go
には、*.proto
で定義したメッセージ対応する struct やその Getter が定義される。
type Person struct {
// 中略
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Id int32 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
Phones []*Person_PhoneNumber `protobuf:"bytes,4,rep,name=phones,proto3" json:"phones,omitempty"`
LastUpdated *timestamp.Timestamp `protobuf:"bytes,5,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"`
}
func (x *Person) GetName() string { /* 中略 */ }
func (x *Person) GetId() int32 { /* 中略 */ }
func (x *Person) GetEmail() string { /* 中略 */ }
func (x *Person) GetPhones() []*Person_PhoneNumber { /* 中略 */ }
func (x *Person) GetLastUpdated() *timestamp.Timestamp { /* 中略 */ }
これらのメッセージに対して、github.com/golang/protobuf/proto
パッケージの Marshal(), Unmarshal() 関数を用いてシリアライズ/デシリアライズを行う。
コードはこの辺りを参照。
ハマったところ
-
*.pb.go
が生成される場所
*.proto ファイルでoption go_package = "github.com/hoge/fuga";
というようにパッケージのフルパスを指定して protoc を実行すると、 --go_out オプションで指定したディレクトリを起点としてgithub.com/hoge/fuga
というディレクトリが作成されて、そこに *.pb.go が生成される。自分が生成したい場所を意識して --go_out または go_package の指定の仕方を調整する必要あり。