Protocol Buffers Go v2 Walkthrough
Protocol Buffers の Go 実装には v1 (v1.20.0 以前) と v2 (v1.20.0 以降) の 2 つの実装があります。
v2 では公式にリフレクションが提供されるといった、大きな機能追加があり、それに伴い設計も大きく変化しています。
この記事では v2 が持つパッケージの関係と責務について順番に紹介していきます。
全体像
v2 は google.golang.org/protobuf モジュールとして提供されており、この配下には以下のようなパッケージがあります。
-
proto
- Protocol Buffers のメッセージを操作するため関数群
-
encoding
- メッセージのエンコード・デコード
-
testing
- テストユーティリティ
-
types
- well-known type や動的なメッセージ型を構築するための機能
-
compiler/protogen
- protoc プラグインを構築するためのヘルパー
-
reflect
- リフレクション機能
proto
proto
はメッセージを操作するための基本的な関数群を提供するパッケージで、v1 の proto
とあまり違いはありません。
Marshal
や Unmarshal
を使い、メッセージのエンコードやデコードを行えます。
encoding
encoding
は JSON やテキスト形式といった別なフォーマットへエンコード・デコードするための機能を提供しています。
protojson
は v1 の jsonpb
に、prototext
は textpb
に相当するパッケージです。
protowire
は Protocol Buffers の wire format を実装しているパッケージで、この API を用いてメッセージをバイト列へエンコードしたり、デコードしています。ユーザが使うことを意図しておらず、通常の使い方であれば代わりに proto.Marshal
や proto.Unmarshal
を使います。
wire format については別の記事でも紹介しているので興味があったら読んでみてください。
testing
testing
はテストに役立つユーティリティパッケージです。
protocmp
は github.com/google/go-cmp のオプションを提供しているパッケージです。
Protocol Buffers の自動生成された型はメタデータ用のフィールドを含むため、単純な比較を行うことができません。しかし、このパッケージの Transform
を使うことによって比較ができるようになります。
protopack
は wire format をもとにテストデータ用のバイナリを生成するためのパッケージです。Marshal
のドキュメントに書いてあるように、フィールド番号や wire type を直接指定してエンコードされたメッセージをつくることができます。
prototest
は対象の型が正しくリフレクションを実装しているかをテストするためのパッケージです。基本的にこのパッケージを使うことはないと思います。
types
types
は型の定義や機能を提供するパッケージです。
known
には well-known type が定義されています。
descriptorpb
には Protocol Buffers の descriptor 定義から自動生成された型が含まれています。
dynamicpb
は動的に (自動生成された型なしに) メッセージをつくるためのパッケージです。
compiler/protogen
protoc プラグインを構築するためのヘルパーパッケージです。Options
型の Run
がエントリポイントになっており、ここにプラグインの本処理を記述します。
実際の使い方については protoc-gen-go のコードを読むのが一番わかりやすいと思います。
reflect
リフレクションを提供するパッケージで、さらに複数のパッケージから構成されています。
protoregistry
は FileDescriptor や MessageDescriptor を登録・ルックアップするための機能を提供しています。グローバル変数に GlobalFiles
と GlobalTypes
があり、それぞれ FileDescriptor と MessageDescriptor の情報を持っています。
proto.Marshal
や proto.Unmarshal
、また Any 型のエンコード・デコードといった、descriptor を必要としている関数がこれらの変数から取得しています。
protoc-gen-go で自動生成された型を持っている場合、init
で protoregistry
に descriptor が登録されているため、普段は意識することはないでしょう。
protoreflect
はリフレクション関連パッケージの中でもっとも中心的なパッケージです。このパッケージでは、リフレクション用の descriptor インターフェースを提供しています。
自動生成された型は ProtoReflect
というメソッドを持っており、このメソッドを呼ぶことで protoreflect.Message
が手に入り、リフレクションの世界へ入ることができます。
protoreflect.Message
が持つ Descriptor
メソッドを呼べば protoreflect.MessageDescriptor
も手に入り、dynamicpb.NewMessage
で動的にメッセージ型をつくるために使えます。
protorange
と protopath
はメッセージ型の値を順番に巡回するためのパッケージです。Examples にいくつか例があるので、こちらを参照するのが一番わかりやすいと思います。
protodesc
は descriptorpb
にある自動生成された型と protoreflect
の型の相互変換を行うためのパッケージです。
jhump/protoreflect
の desc
パッケージを使っていると、名前が似ているので同様のことを提供していると勘違いしがちですが、jhump/protoreflect/desc
パッケージと同じような機能を提供しているのは protoreflect
パッケージです。
リフレクション関係図
リフレクション関連のパッケージの依存関係は以下のようになっています。複雑ですね。
この図からも分かる通り、リフレクション関連機能は常に protoreflect
に依存しています。そのため、リフレクションを行うにはなにはともあれ protoreflect
で定義されている descriptor 型を体に入れる必要があります。
すでに自動生成されている型を持っているのであれば、そこからリフレクションに入れます。自動生成された型がない場合 (e.g. gRPC リフレクション、proto ファイルをパースしている) は descriptorpb
の型を protodesc
によって protoreflect
の descriptor へ変換する必要があります。
また、同様に自動生成されている型がない場合、エンコード・デコードに必要な descriptor が protoregistry
にまだ登録されていません。そういった場合は protoregistry.GlobalTypes
へ明示的に登録を行う必要があります。
まとめ
だいぶ大雑把にですが各パッケージの紹介をしました。知っていると普段の開発に役立つパッケージも多いので、ぜひこれらのパッケージを頭の片隅に覚えていてください。
Discussion