Protocol Bufferの基本:Go
このチュートリアルでは、proto3バージョンのProtocol Buffer言語を使用して、Goプログラマー向けのProtocol Bufferを扱うための基本的な紹介を提供します。簡単なサンプルアプリケーションの作成を通して、次の方法を説明します。
.protoファイルでメッセージ形式を定義する。- protocol buffer コンパイラを使用する。
- Go Protocol Buffer APIを使用してメッセージを書き込み、読み取る。
これは、GoでProtocol Bufferを使用するための包括的なガイドではありません。より詳細なリファレンス情報については、「Protocol Buffer言語ガイド」、「Go APIリファレンス」、「Go生成コードガイド」、「エンコーディングリファレンス」を参照してください。
問題領域
ここで使用する例は、人々の連絡先の詳細をファイルに読み書きできる、非常にシンプルな「アドレス帳」アプリケーションです。アドレス帳の各人物には、名前、ID、メールアドレス、連絡先の電話番号があります。
このような構造化データをどのようにシリアライズし、取得するのでしょうか? この問題を解決するにはいくつかの方法があります。
- Goデータ構造をシリアル化するには、gobsを使用します。これはGo固有の環境では良い解決策ですが、他のプラットフォーム用に書かれたアプリケーションとデータを共有する必要がある場合にはうまく機能しません。
- データ項目を単一の文字列にエンコードするアドホックな方法を考案することもできます。たとえば、「12:3:-23:67」として4つの整数をエンコードするような方法です。これはシンプルで柔軟なアプローチですが、一度限りのエンコードおよび解析コードの記述が必要であり、解析にはわずかな実行時コストがかかります。これは、非常に単純なデータをエンコードする場合に最適です。
- データをXMLにシリアル化します。このアプローチは、XMLが(ある程度)人間が読める形式であり、多くの言語向けにバインディングライブラリがあるため、非常に魅力的です。他のアプリケーション/プロジェクトとデータを共有したい場合には良い選択肢となります。しかし、XMLはサイズが大きくなることで悪名高く、エンコード/デコードにはアプリケーションに多大なパフォーマンスペナルティを課す可能性があります。また、XML DOMツリーをナビゲートすることは、クラス内の単純なフィールドを通常ナビゲートするよりもかなり複雑です。
プロトコルバッファは、まさにこの問題を解決するための柔軟で効率的な自動化されたソリューションです。プロトコルバッファを使用すると、保存したいデータ構造の ` .proto ` 記述を記述します。そこから、プロトコルバッファコンパイラは、効率的なバイナリ形式でプロトコルバッファデータの自動エンコーディングとパースを実装するクラスを作成します。生成されたクラスは、プロトコルバッファを構成するフィールドのゲッターとセッターを提供し、プロトコルバッファを1つの単位として読み書きする詳細を処理します。重要なことに、プロトコルバッファ形式は、コードが古い形式でエンコードされたデータをまだ読み取れるような方法で、時間の経過とともに形式を拡張するという考え方をサポートしています。
サンプルコードの場所
私たちの例は、プロトコルバッファを使用してエンコードされたアドレス帳データファイルを管理するためのコマンドラインアプリケーションのセットです。` add_person_go ` コマンドはデータファイルに新しいエントリを追加します。` list_people_go ` コマンドはデータファイルを解析し、データをコンソールに出力します。
完全な例は、GitHub リポジトリの examples ディレクトリにあります。
プロトコルフォーマットの定義
アドレス帳アプリケーションを作成するには、まず ` .proto ` ファイルから始める必要があります。` .proto ` ファイルの定義はシンプルです。シリアル化したいデータ構造ごとに メッセージ を追加し、そのメッセージ内の各フィールドに名前と型を指定します。私たちの例では、メッセージを定義する ` .proto ` ファイルは ` addressbook.proto ` です。
.proto ファイルはパッケージ宣言から始まります。これは、異なるプロジェクト間での名前の衝突を防ぐのに役立ちます。
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
` go_package ` オプションは、このファイル用に生成されたすべてのコードを含むパッケージのインポートパスを定義します。Goパッケージ名は、インポートパスの最後のパスコンポーネントになります。たとえば、私たちの例では「tutorialpb」というパッケージ名を使用します。
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";
次に、メッセージ定義があります。メッセージは、型付けされたフィールドのセットを含む単なる集約体です。` bool `、` int32 `、` float `、` double `、` string ` など、多くの標準的な単純なデータ型がフィールド型として利用可能です。他のメッセージ型をフィールド型として使用することで、メッセージにさらに構造を追加することもできます。
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
上記の例では、` Person ` メッセージは ` PhoneNumber ` メッセージを含み、` AddressBook ` メッセージは ` Person ` メッセージを含んでいます。他のメッセージの中にネストされたメッセージ型を定義することもできます。ご覧のとおり、` PhoneNumber ` 型は ` Person ` の中に定義されています。フィールドの1つが事前に定義された値のリストのいずれかを持つようにしたい場合は、` enum ` 型を定義することもできます。ここでは、電話番号が ` PHONE_TYPE_MOBILE `、` PHONE_TYPE_HOME `、または ` PHONE_TYPE_WORK ` のいずれかであることを指定したいとします。
各要素に付いている「= 1」、「= 2」のマーカーは、バイナリエンコーディングでそのフィールドが使用する一意の「タグ」を識別します。タグ番号1~15は、それ以上の番号よりもエンコードに必要なバイト数が1バイト少なくなるため、最適化として、これらを頻繁に使用される要素や繰り返し使用される要素に割り当て、タグ16以降をあまり頻繁に使用されないオプションの要素に割り当てることができます。繰り返しフィールドの各要素はタグ番号の再エンコードを必要とするため、繰り返しフィールドは特にこの最適化に適した候補です。
フィールド値が設定されていない場合、デフォルト値が使用されます。数値型の場合はゼロ、文字列の場合は空の文字列、ブール値の場合はfalseです。埋め込みメッセージの場合、デフォルト値は常にメッセージの「デフォルトインスタンス」または「プロトタイプ」であり、そのフィールドは何も設定されていません。明示的に設定されていないフィールドの値を取得するためにアクセサーを呼び出すと、常にそのフィールドのデフォルト値が返されます。
フィールドが repeated の場合、そのフィールドは任意の回数(ゼロを含む)繰り返すことができます。繰り返される値の順序は Protocol Buffer に保存されます。repeated フィールドは動的にサイズ変更される配列と考えてください。
プロトコルバッファ言語ガイドには、すべての可能なフィールド型を含む、` .proto ` ファイルの書き方に関する完全なガイドがあります。ただし、クラス継承に似た機能を探さないでください。プロトコルバッファにはそのような機能はありません。
Protocol Buffer のコンパイル
` .proto ` ファイルができたら、次に必要なことは、` AddressBook ` (そして ` Person ` と ` PhoneNumber `) メッセージを読み書きするために必要なクラスを生成することです。これを行うには、` .proto ` に対してプロトコルバッファコンパイラ ` protoc ` を実行する必要があります。
コンパイラをインストールしていない場合は、パッケージをダウンロードし、README の指示に従ってください。
Go Protocol Bufferプラグインをインストールするには、次のコマンドを実行します。
go install google.golang.org/protobuf/cmd/protoc-gen-go@latestコンパイラプラグイン ` protoc-gen-go ` は `$GOBIN ` にインストールされます(デフォルトは `$GOPATH/bin `)。プロトコルコンパイラ ` protoc ` がそれを見つけるためには、` $PATH ` に含まれている必要があります。
次に、コンパイラを実行し、ソースディレクトリ(アプリケーションのソースコードがある場所。値を指定しない場合は現在のディレクトリが使用されます)、出力先ディレクトリ(生成されたコードの出力先。多くの場合 `$SRC_DIR ` と同じです)、および ` .proto ` へのパスを指定します。この場合、次のように呼び出します。
protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.protoGoコードが必要なため、` --go_out ` オプションを使用します。他のサポートされている言語にも同様のオプションが提供されています。
これにより、指定された出力先ディレクトリに ` github.com/protocolbuffers/protobuf/examples/go/tutorialpb/addressbook.pb.go ` が生成されます。
Protocol Buffer API
` addressbook.pb.go ` の生成により、以下の便利な型が提供されます。
- ` People ` フィールドを持つ ` AddressBook ` 構造体。
- ` Name `、` Id `、` Email `、` Phones ` の各フィールドを持つ ` Person ` 構造体。
- ` Number ` と ` Type ` のフィールドを持つ ` Person_PhoneNumber ` 構造体。
- ` Person_PhoneType ` 型と、` Person.PhoneType ` enum の各値に定義された値。
生成される内容の詳細については、「Go生成コードガイド」で確認できますが、ほとんどの場合、これらはごく普通のGoの型として扱えます。
` list_people ` コマンドの単体テストからの例で、` Person ` のインスタンスを作成する方法を示します。
p := pb.Person{
Id: 1234,
Name: "John Doe",
Email: "jdoe@example.com",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-4321", Type: pb.PhoneType_PHONE_TYPE_HOME},
},
}
メッセージの書き込み
プロトコルバッファを使用する目的は、データをシリアライズして他の場所で解析できるようにすることです。Goでは、` proto ` ライブラリの Marshal 関数を使用してプロトコルバッファデータをシリアライズします。プロトコルバッファメッセージの ` struct ` へのポインタは、` proto.Message ` インターフェースを実装します。` proto.Marshal ` を呼び出すと、ワイヤー形式でエンコードされたプロトコルバッファが返されます。たとえば、この関数は ` add_person ` コマンドで使用されています。
book := &pb.AddressBook{}
// ...
// Write the new address book back to disk.
out, err := proto.Marshal(book)
if err != nil {
log.Fatalln("Failed to encode address book:", err)
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
log.Fatalln("Failed to write address book:", err)
}
メッセージの読み取り
エンコードされたメッセージを解析するには、` proto ` ライブラリの Unmarshal 関数を使用します。これを呼び出すと、` in ` のデータがプロトコルバッファとして解析され、結果が ` book ` に配置されます。したがって、` list_people ` コマンドでファイルを解析するには、次のようにします。
// Read the existing address book.
in, err := ioutil.ReadFile(fname)
if err != nil {
log.Fatalln("Error reading file:", err)
}
book := &pb.AddressBook{}
if err := proto.Unmarshal(in, book); err != nil {
log.Fatalln("Failed to parse address book:", err)
}
Protocol Buffer の拡張
プロトコルバッファを使用するコードをリリースした後、遅かれ早かれプロトコルバッファの定義を「改善」したくなるでしょう。新しいバッファが後方互換性を持ち、古いバッファが前方互換性を持つことを望むのであれば(そしてほとんどの場合そうでしょう)、従うべきルールがいくつかあります。プロトコルバッファの新しいバージョンでは:
- 既存のフィールドのタグ番号を変更しては*いけません*。
- フィールドを削除しても*かまいません*。
- 新しいフィールドを追加しても*かまいません*が、新しいタグ番号(つまり、この Protocol Buffer で一度も使用されていないタグ番号。削除されたフィールドが使用していたものも含む)を使用しなければなりません。
(これらのルールにはいくつかの例外がありますが、めったに使用されません。)
これらのルールに従えば、古いコードは新しいメッセージを問題なく読み込み、新しいフィールドは単純に無視します。古いコードにとって、削除された単一フィールドは単にデフォルト値になり、削除された繰り返しフィールドは空になります。新しいコードも古いメッセージを透過的に読み込みます。
ただし、新しいフィールドは古いメッセージには存在しないことに注意してください。したがって、デフォルト値で何か合理的な処理を行う必要があります。型固有のデフォルト値が使用されます。文字列の場合、デフォルト値は空の文字列です。ブール型の場合、デフォルト値は false です。数値型の場合、デフォルト値はゼロです。