Go ブログ

Protocol Buffers 用の新しい Go API

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

はじめに

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 の型システムの一部ではありません。

もう 1 つの一般的な要望は、コンパイル時に型が不明なメッセージを表すことができる動的メッセージ型など、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 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は値とエラーの両方を返しました。新しいproto.GetExtensionは値のみを返し、存在しない場合はフィールドのデフォルト値を返します。拡張機能のデコードエラーは Unmarshal 時に報告されます。

編集が必要なフィールドを特定したら、クリアするのは簡単です

m.Clear(fd)

上記をすべてまとめると、完全な編集関数は次のようになります

// Redact clears every sensitive field in 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 API の元のバージョンを APIv1、新しいバージョンを APIv2 と呼びます。APIv2 は APIv1 との下位互換性がないため、それぞれに異なるモジュールパスを使用する必要があります。

(これらの API バージョンは、Protocol Buffer 言語のバージョン (proto1proto2、および proto3) と同じではありません。APIv1 と APIv2 は、proto2proto3 の両方の言語バージョンをサポートする 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 は、APIv2 より前の APIv1 の最新バージョンです。

  • github.com/golang/protobuf@v1.4.0 は、APIv2 の観点から実装された APIv1 のバージョンです。API は同じですが、基盤となる実装は新しい実装によってバックアップされています。このバージョンには、2 つの間の移行を容易にするために、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マッピングを使用してプロトコルバッファメッセージをJSONとの間で変換します。また、既存のユーザーに問題を引き起こすことなく変更することが困難であった、古いjsonpbパッケージの多くの問題を修正します。

google.golang.org/protobuf/types/dynamicpb パッケージは、プロトコルバッファ型が実行時に派生するメッセージに対してproto.Messageの実装を提供します。

google.golang.org/protobuf/testing/protocmp パッケージは、github.com/google/cmp パッケージを使用してプロトコルバッファメッセージを比較する機能を提供します。

google.golang.org/protobuf/compiler/protogen パッケージは、プロトコルコンパイラプラグインの作成をサポートします。

結論

google.golang.org/protobufモジュールは、Goのプロトコルバッファのサポートを大幅に見直し、リフレクション、カスタムメッセージの実装、そしてクリーンアップされたAPIサーフェスに対するファーストクラスのサポートを提供します。以前のAPIを新しいAPIのラッパーとして無期限に維持する予定であり、ユーザーは自分のペースで新しいAPIを段階的に採用できます。

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

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

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