👀

Protocol Buffers Go v2 Walkthrough

2022/04/17に公開

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 とあまり違いはありません。
MarshalUnmarshal を使い、メッセージのエンコードやデコードを行えます。

encoding

encoding は JSON やテキスト形式といった別なフォーマットへエンコード・デコードするための機能を提供しています。

protojson は v1 の jsonpb に、prototexttextpb に相当するパッケージです。
protowire は Protocol Buffers の wire format を実装しているパッケージで、この API を用いてメッセージをバイト列へエンコードしたり、デコードしています。ユーザが使うことを意図しておらず、通常の使い方であれば代わりに proto.Marshalproto.Unmarshal を使います。

wire format については別の記事でも紹介しているので興味があったら読んでみてください。

https://engineering.mercari.com/blog/entry/20210921-ca19c9f371/

testing

testing はテストに役立つユーティリティパッケージです。

protocmpgithub.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 を登録・ルックアップするための機能を提供しています。グローバル変数に GlobalFilesGlobalTypes があり、それぞれ FileDescriptor と MessageDescriptor の情報を持っています。
proto.Marshalproto.Unmarshal、また Any 型のエンコード・デコードといった、descriptor を必要としている関数がこれらの変数から取得しています。
protoc-gen-go で自動生成された型を持っている場合、initprotoregistry に descriptor が登録されているため、普段は意識することはないでしょう。

protoreflect はリフレクション関連パッケージの中でもっとも中心的なパッケージです。このパッケージでは、リフレクション用の descriptor インターフェースを提供しています。

自動生成された型は ProtoReflect というメソッドを持っており、このメソッドを呼ぶことで protoreflect.Message が手に入り、リフレクションの世界へ入ることができます。
protoreflect.Message が持つ Descriptor メソッドを呼べば protoreflect.MessageDescriptor も手に入り、dynamicpb.NewMessage で動的にメッセージ型をつくるために使えます。

protorangeprotopath はメッセージ型の値を順番に巡回するためのパッケージです。Examples にいくつか例があるので、こちらを参照するのが一番わかりやすいと思います。

protodescdescriptorpb にある自動生成された型と protoreflect の型の相互変換を行うためのパッケージです。

jhump/protoreflectdesc パッケージを使っていると、名前が似ているので同様のことを提供していると勘違いしがちですが、jhump/protoreflect/desc パッケージと同じような機能を提供しているのは protoreflect パッケージです。

リフレクション関係図

リフレクション関連のパッケージの依存関係は以下のようになっています。複雑ですね。

この図からも分かる通り、リフレクション関連機能は常に protoreflect に依存しています。そのため、リフレクションを行うにはなにはともあれ protoreflect で定義されている descriptor 型を体に入れる必要があります。

すでに自動生成されている型を持っているのであれば、そこからリフレクションに入れます。自動生成された型がない場合 (e.g. gRPC リフレクション、proto ファイルをパースしている) は descriptorpb の型を protodesc によって protoreflect の descriptor へ変換する必要があります。

また、同様に自動生成されている型がない場合、エンコード・デコードに必要な descriptor が protoregistry にまだ登録されていません。そういった場合は protoregistry.GlobalTypes へ明示的に登録を行う必要があります。

まとめ

だいぶ大雑把にですが各パッケージの紹介をしました。知っていると普段の開発に役立つパッケージも多いので、ぜひこれらのパッケージを頭の片隅に覚えていてください。

Discussion