Protocol Buffers向けの新しいGo API
A new Go API for Protocol Buffers by Joe Tsai, Damien Neil, and Herbie Ong
はじめに
Googleの言語非依存なデータ交換形式である protocol buffers 向けのGo APIの大規模な改訂版をリリースできることを嬉しく思います。
新しいAPIを作った動機
Go向けの最初のprotocol bufferバインディングは、2010年3月に Rob Pikeによって発表されました。 Go 1がリリースされるのは、それからさらに2年後のことです。
その最初のリリースから10年の間に、このパッケージはGoとともに成長し発展してきました。 そしてユーザーの要求もまた大きくなってきました。
多くの人が、リフレクションを使ってprotocol bufferのメッセージを調べるプログラムを書きたいと考えています。
reflect
パッケージはGoの型と値についてのビューを提供しますが、protocol bufferの型システムが持つ情報は欠落しています。
たとえば、ログエントリを走査して、機微な情報を含むと注釈が付けられたフィールドをすべてクリアする関数を書きたいとしましょう。
その注釈はGoの型システムの一部にはなっていません。
もう一つよくある要望は、protocol bufferコンパイラが生成するデータ構造とは別のデータ構造、 たとえばコンパイル時に型がわからないメッセージを表現できる動的なメッセージ型を使いたいというものです。
さらに私たちは、生成されたメッセージ型の値を識別するインターフェースである
proto.Message
が、その型の振る舞いをほとんど記述していないことが問題の頻発する原因になっていることにも気づきました。
ユーザーがそのインターフェースを実装する型を作り(多くの場合、メッセージを別の構造体に埋め込むことで意図せずそうなってしまいます)、
それらの値を生成されたメッセージ値を期待する関数に渡すと、プログラムがクラッシュしたり予測不能な振る舞いをしたりします。
これら3つの問題にはいずれも共通の原因があり、共通の解決策があります。
Message インターフェースはメッセージの振る舞いを完全に規定するべきであり、
Message 値を扱う関数はそのインターフェースを正しく実装するあらゆる型を自由に受け付けるべきである、ということです。
パッケージのAPI互換性を保ったまま既存の Message 型の定義を変更することはできないため、
私たちはprotobufモジュールの新しい、互換性のないメジャーバージョンの開発に着手する時が来たと判断しました。
そして本日、私たちはその新しいモジュールをリリースします。気に入っていただけると幸いです。
リフレクション
リフレクションは新しい実装の目玉機能です。 reflect パッケージがGoの型と値についてのビューを提供するのと同様に、
google.golang.org/protobuf/reflect/protoreflect
パッケージは、protocol bufferの型システムに従った値のビューを提供します。
protoreflect パッケージの完全な説明はこの記事には長すぎるので、
先ほど触れたログを消去する関数をどのように書けるか見てみましょう。
まず、
google.protobuf.FieldOptions
型を拡張する .proto ファイルを書いて、フィールドに機微な情報を含むかどうかを注釈できるようにします。
syntax = "proto3";
import "google/protobuf/descriptor.proto";
package golang.example.policy;
extend google.protobuf.FieldOptions {
bool non_sensitive = 50000;
}
このオプションを使って、特定のフィールドを機微でないものとしてマークできます。
message MyMessage {
string public_name = 1 [(golang.example.policy.non_sensitive) = true];
}
次に、任意のメッセージ値を受け取り、機微なフィールドをすべて取り除くGoの関数を書きます。
// Redact は pb 内の機微なフィールドをすべてクリアします。
func Redact(pb proto.Message) {
// ...
}
この関数は、生成されたすべてのメッセージ型が実装するインターフェース型である
proto.Message
を受け取ります。この型は protoreflect パッケージで定義されている型のエイリアスです。
type ProtoMessage interface{
ProtoReflect() Message
}
生成されたメッセージの名前空間を埋め尽くさないよう、このインターフェースはメッセージの内容へのアクセスを提供する
protoreflect.Message
を返す、たった一つのメソッドだけを持っています。
(なぜエイリアスなのでしょうか。protoreflect.Message には元の proto.Message を返す対応するメソッドがあり、
2つのパッケージ間でのインポート循環を避ける必要があるからです。)
protoreflect.Message.Range
メソッドは、メッセージ内の値が入っているフィールドそれぞれに対して関数を呼び出します。
m := pb.ProtoReflect()
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
// ...
return true
})
このrange関数には、フィールドのprotocol bufferの型を記述する
protoreflect.FieldDescriptor
と、フィールドの値を保持する
protoreflect.Value
が渡されます。
protoreflect.FieldDescriptor.Options
メソッドは、フィールドのオプションを google.protobuf.FieldOptions メッセージとして返します。
opts := fd.Options().(*descriptorpb.FieldOptions)
(なぜ型アサーションが必要なのでしょうか。生成された descriptorpb パッケージは protoreflect に依存しているため、
protoreflect パッケージはインポート循環を起こさずに具体的なオプションの型を返すことができないからです。)
そして、そのオプションを見て、拡張したbool値を確認できます。
if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
return true // 機微でないフィールドはリダクトしない
}
ここで見ているのはフィールドの 値 ではなく、フィールドの ディスクリプタ であることに注意してください。 私たちが関心を持っている情報は、Goの型システムではなく、protocol bufferの型システムの中にあります。
これはまた、私たちが proto パッケージのAPIを簡素化した部分の一例でもあります。
元の
proto.GetExtension
は値とエラーの両方を返していました。新しい
proto.GetExtension
は値だけを返し、フィールドが存在しない場合はそのフィールドのデフォルト値を返します。
拡張のデコードエラーは Unmarshal の時点で報告されます。
リダクトが必要なフィールドを特定できれば、あとはそれをクリアするだけです。
m.Clear(fd)
ここまでのすべてをまとめると、完全なリダクション関数は次のようになります。
// Redact は pb 内の機微なフィールドをすべてクリアします。
func Redact(pb proto.Message) {
m := pb.ProtoReflect()
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
opts := fd.Options().(*descriptorpb.FieldOptions)
if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
return true
}
m.Clear(fd)
return true
})
}
より完全な実装であれば、メッセージ型のフィールドを再帰的に辿ることになるでしょう。 この単純な例が、protocol bufferのリフレクションとその使いどころの雰囲気を伝えられていれば幸いです。
バージョン
私たちはGo protocol buffersの元のバージョンをAPIv1、新しいバージョンをAPIv2と呼んでいます。 APIv2はAPIv1と後方互換性がないため、それぞれに異なるモジュールパスを使う必要があります。
(このAPIバージョンは、protocol buffer言語のバージョンである proto1、proto2、proto3 とは異なるものです。
APIv1とAPIv2はどちらも、proto2 と proto3 の言語バージョンをサポートするGoでの具体的な実装です。)
github.com/golang/protobuf
モジュールがAPIv1です。
google.golang.org/protobuf
モジュールがAPIv2です。私たちはインポートパスを変更する必要があることを利用して、
特定のホスティングプロバイダーに紐づかないパスに切り替えました。
(これがAPIの2番目のメジャーバージョンであることを明確にするため google.golang.org/protobuf/v2
も検討しましたが、長期的にはより短いパスの方が良い選択だと判断しました。)
すべてのユーザーが同じ速さでパッケージの新しいメジャーバージョンに移行するわけではないことを、 私たちはわかっています。すぐに切り替える人もいれば、旧バージョンに無期限に留まる人もいるでしょう。 1つのプログラムの中であっても、一部は片方のAPIを使い、他の部分はもう片方を使うということもあり得ます。 それゆえ、私たちはAPIv1を使うプログラムのサポートをやめるわけにはいきません。
github.com/golang/protobuf@v1.3.4は、APIv2より前の、APIv1の最新版です。github.com/golang/protobuf@v1.4.0は、APIv2を用いて実装されたAPIv1のバージョンです。 APIは同じですが、内部の実装は新しいものに置き換わっています。 このバージョンには、APIv1とAPIv2のproto.Messageインターフェース間を変換する関数が含まれており、 両者の間の移行を容易にします。google.golang.org/protobuf@v1.20.0はAPIv2です。 このモジュールはgithub.com/golang/protobuf@v1.4.0に依存しているため、 APIv2を使うプログラムはすべて、それと統合されたバージョンのAPIv1を自動的に選択することになります。
(なぜ v1.20.0 から始めるのでしょうか。わかりやすくするためです。
私たちはAPIv1が v1.20.0 に到達することはないと見込んでいるので、
バージョン番号だけでAPIv1とAPIv2を曖昧さなく区別できるはずです。)
私たちはAPIv1のサポートを無期限に続けるつもりです。
この構成により、どのAPIバージョンを使っていても、あるプログラムが使うprotocol bufferの実装は 常にただ一つに定まることが保証されます。これにより、プログラムは新しいAPIの利点を享受しながらも、 新しいAPIを段階的に、あるいはまったく採用しないという選択もできます。 最小バージョン選択の原則により、プログラムはメンテナが(直接、あるいは依存関係の更新を通じて) 新しい実装に更新することを選ぶまでは、古い実装のままでいられます。
その他の注目すべき機能
google.golang.org/protobuf/encoding/protojson
パッケージは、
正規のJSONマッピング
を使ってprotocol bufferのメッセージとJSONを相互変換します。
既存のユーザーに影響を与えずに変更するのが難しかった、古い jsonpb パッケージの数多くの問題も修正されています。
google.golang.org/protobuf/types/dynamicpb
パッケージは、実行時に導出されるprotocol buffer型を持つメッセージ向けの proto.Message の実装を提供します。
google.golang.org/protobuf/testing/protocmp
パッケージは、
github.com/google/cmp
パッケージを使ってprotocol bufferのメッセージを比較する関数を提供します。
google.golang.org/protobuf/compiler/protogen
パッケージは、protocolコンパイラのプラグインを書くためのサポートを提供します。
まとめ
google.golang.org/protobuf モジュールは、Goのprotocol buffersサポートを大幅に見直したものであり、
リフレクション、カスタムメッセージの実装、整理されたAPI表面を本格的にサポートします。
私たちは、以前のAPIを新しいAPIのラッパーとして無期限に維持するつもりであり、
ユーザーは自分のペースで段階的に新しいAPIを採用できます。
この更新における私たちの目標は、古いAPIの利点を伸ばしつつ、その欠点に対処することです。 新しい実装の各コンポーネントが完成するたびに、私たちはそれをGoogleのコードベース内で実際に使ってきました。 この段階的な展開によって、新しいAPIの使いやすさと、新しい実装のパフォーマンスおよび正しさの両方について 私たちは自信を深めました。これはプロダクション環境で使える品質だと確信しています。
私たちはこのリリースに胸を躍らせていますし、これがこの先10年、あるいはそれ以上の間、 Goのエコシステムに貢献してくれることを願っています。
By Joe Tsai, Damien Neil, and Herbie Ong