The Go Blog

Protocol Buffers 用の新しい Go API

Joe Tsai、Damien Neil、Herbie Ong
2020年3月2日

はじめに

Google の言語に依存しないデータ交換フォーマットである Protocol Buffers の Go API の大幅な改訂版のリリースを発表できることを嬉しく思います。

新しい API の動機

Go 用の最初の Protocol Buffer バインディングは、Rob Pike によって2010年3月に 発表されました。Go 1 がリリースされるのは、そのさらに2年後でした。

最初のリリースから10年間で、パッケージは Go とともに成長し、発展してきました。ユーザーの要件も増大しました。

多くの人は、リフレクションを使用して Protocol Buffer メッセージを検査するプログラムを書きたいと考えています。reflect パッケージは Go の型と値のビューを提供しますが、Protocol Buffer の型システムからの情報は省略されています。たとえば、ログエントリを走査し、機密データを含むものとして注釈が付けられたフィールドをすべてクリアする関数を書きたいとします。注釈は Go の型システムの一部ではありません。

もう1つの一般的な要望は、コンパイル時に型が不明なメッセージを表すことができる動的メッセージ型など、Protocol Buffer コンパイラによって生成されるもの以外のデータ構造を使用することです。

また、問題の頻繁な原因として、生成されたメッセージ型の値を識別する proto.Message インターフェースが、それらの型の動作をほとんど記述していないことが挙げられます。ユーザーがそのインターフェースを実装する型を作成し(多くの場合、メッセージを別の構造体に埋め込むことによって不注意で)、それらの型の値を生成されたメッセージ値を期待する関数に渡すと、プログラムはクラッシュしたり、予期せず動作したりします。

これら3つの問題はすべて共通の原因と共通の解決策を持っています。それは、Message インターフェースがメッセージの動作を完全に指定し、Message 値を操作する関数は、インターフェースを正しく実装する任意の型を自由に受け入れるべきである、というものです。

既存の Message 型の定義をパッケージ API の互換性を保ちながら変更することは不可能であるため、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 clears every sensitive field in 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
})

レンジ関数は、フィールドの Protocol Buffer 型を記述する protoreflect.FieldDescriptor と、フィールド値を含む protoreflect.Value を引数として呼び出されます。

protoreflect.FieldDescriptor.Options メソッドは、フィールドオプションを google.protobuf.FieldOptions メッセージとして返します。

opts := fd.Options().(*descriptorpb.FieldOptions)

(なぜ型アサーションなのか?生成された descriptorpb パッケージが protoreflect に依存しているため、protoreflect パッケージはインポートサイクルを引き起こすことなく具体的なオプション型を返すことができないためです。)

次に、オプションをチェックして拡張ブール値を確認できます。

if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
    return true // don't redact non-sensitive fields
}

ここで見ているのはフィールドの*記述子*であり、フィールドの*値*ではないことに注意してください。私たちが興味を持っている情報は、Go の型システムではなく、Protocol Buffer の型システムにあります。

これは、proto パッケージ API を簡素化した領域の一例でもあります。元の proto.GetExtension は値とエラーの両方を返しました。新しい バージョン

Go Protocol Buffers の元のバージョンを APIv1、新しいバージョンを APIv2 と呼びます。APIv2 は APIv1 と後方互換性がないため、それぞれ異なるモジュールパスを使用する必要があります。

(これらの API バージョンは、Protocol Buffer 言語のバージョン(proto1proto2proto3)と同じではありません。APIv1 と APIv2 は、どちらも proto2 および proto3 言語バージョンをサポートする Go の具体的な実装です。)

github.com/golang/protobuf モジュールは APIv1 です。

google.golang.org/protobuf モジュールは APIv2 です。インポートパスを変更する必要性を利用して、特定のホスティングプロバイダーに縛られないパスに切り替えました。(これが API の2番目のメジャーバージョンであることを明確にするために google.golang.org/protobuf/v2 を検討しましたが、長期的には短いパスの方が良い選択であると判断しました。)

すべてのユーザーが同じ速度でパッケージの新しいメジャーバージョンに移行するわけではないことを認識しています。一部のユーザーはすぐに切り替えるでしょう。他のユーザーは古いバージョンに無期限にとどまるかもしれません。単一のプログラム内でも、一部のパーツは1つの API を使用し、他のパーツは別の API を使用する可能性があります。したがって、APIv1 を使用するプログラムのサポートを継続することが不可欠です。

  • github.com/golang/protobuf@v1.3.4 は、APIv1 の最新の APIv2 以前のバージョンです。

  • 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 を段階的に採用するか、まったく採用しないかを選択できると同時に、新しい実装の利点を得ることができます。最小バージョン選択の原則により、プログラムはメンテナーが新しい実装に更新する(直接または依存関係を更新することによって)まで、古い実装にとどまることができます。

注目すべき追加機能

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 Compiler プラグインの記述をサポートします。

まとめ

google.golang.org/protobuf モジュールは、Go の Protocol Buffers のサポートを大幅に刷新したもので、リフレクション、カスタムメッセージ実装、および整理された API サーフェスをファーストクラスでサポートします。以前の API は、新しい API のラッパーとして無期限に維持し、ユーザーが自分のペースで新しい API を段階的に採用できるようにする予定です。

この更新の目標は、古い API の利点を改善しつつ、その欠点に対処することです。新しい実装の各コンポーネントが完成するたびに、Google のコードベース内で使用してきました。この段階的な展開により、新しい API の使いやすさ、および新しい実装のパフォーマンスと正確性の両方に自信を持つことができました。本番環境に対応していると確信しています。

このリリースに興奮しており、今後10年以上 Go エコシステムに貢献することを願っています!

次の記事:Go、Go コミュニティ、そしてパンデミック
前の記事:Go 1.14 がリリースされました
ブログインデックス