Protocol Buffer の基本: Go

プロトコルバッファを扱うGoプログラマー向けの基本的な紹介。

このチュートリアルでは、プロトコルバッファの proto3 バージョンを使用して、プロトコルバッファを扱う Go プログラマー向けの基本的な紹介を提供します。簡単なサンプルアプリケーションを作成する手順を通して、次の方法を示します。

  • .proto ファイルでメッセージ形式を定義する。
  • プロトコルバッファコンパイラを使用する。
  • Go プロトコルバッファ API を使用してメッセージを書き込み、読み取る。

これは、Go でプロトコルバッファを使用するための包括的なガイドではありません。詳細なリファレンス情報については、プロトコルバッファ言語ガイドGo API リファレンスGo 生成コードガイド、およびエンコーディングリファレンスを参照してください。

問題領域

使用する例は、人々の連絡先の詳細をファイルに読み書きできる、非常にシンプルな「アドレス帳」アプリケーションです。アドレス帳の各人物には、名前、ID、メールアドレス、連絡先電話番号があります。

このような構造化データをシリアライズして取得するにはどうすればよいでしょうか?この問題を解決する方法はいくつかあります。

  • Go のデータ構造をシリアライズするには、gobs を使用します。これは Go 専用の環境では良い解決策ですが、他のプラットフォーム用に書かれたアプリケーションとデータを共有する必要がある場合にはうまく機能しません。
  • データを単一の文字列にエンコードするアドホックな方法を考案できます。たとえば、4つの整数を「12:3:-23:67」のようにエンコードします。これはシンプルで柔軟なアプローチですが、一度限りのエンコードとパースのコードを書く必要があり、パースにはわずかな実行時コストがかかります。これは、非常にシンプルなデータのエンコードに最適です。
  • データをXMLにシリアライズします。このアプローチは、XMLが(ある程度)人間が読める形式であり、多くの言語にバインディングライブラリがあるため、非常に魅力的です。他のアプリケーション/プロジェクトとデータを共有したい場合に良い選択肢となります。ただし、XMLは非常にスペース効率が悪く、そのエンコード/デコードはアプリケーションに大きなパフォーマンス上のペナルティを課す可能性があります。また、XML DOMツリーをナビゲートすることは、クラス内の単純なフィールドを通常ナビゲートするよりもはるかに複雑です。

プロトコルバッファは、まさにこの問題を解決するための柔軟で効率的な自動化されたソリューションです。プロトコルバッファを使用すると、保存したいデータ構造の .proto 記述を作成します。そこから、プロトコルバッファコンパイラは、効率的なバイナリ形式でプロトコルバッファデータの自動エンコードとパースを実装するクラスを作成します。生成されたクラスは、プロトコルバッファを構成するフィールドのゲッターとセッターを提供し、プロトコルバッファを単位として読み書きする詳細を処理します。重要なのは、プロトコルバッファ形式が、コードが古い形式でエンコードされたデータを引き続き読み取れるように、時間の経過とともに形式を拡張するという考えをサポートしていることです。

サンプルコードの場所

私たちの例は、プロトコルバッファを使用してエンコードされたアドレス帳データファイルを管理するためのコマンドラインアプリケーションのセットです。コマンド 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";

次に、メッセージ定義があります。メッセージは、型付けされたフィールドのセットを含む単なる集合体です。boolint32floatdoublestring など、多くの標準的な単純なデータ型がフィールド型として利用できます。また、他のメッセージ型をフィールド型として使用することで、メッセージにさらなる構造を追加することもできます。

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 の中に定義されています。また、フィールドが事前に定義された値のリストのいずれかを持つようにしたい場合は、enum 型を定義することもできます。ここでは、電話番号が PHONE_TYPE_MOBILEPHONE_TYPE_HOME、または PHONE_TYPE_WORK のいずれかであることを指定したい場合です。

各要素の「= 1」、「= 2」マーカーは、バイナリエンコーディングでそのフィールドが使用する一意の「タグ」を識別します。タグ番号1~15は、それ以上の番号よりもエンコードに1バイト少なくて済むため、最適化として、これらを頻繁に使用される要素や繰り返し要素に使用し、16以上のタグはあまり頻繁に使用されないオプション要素のために残しておくことができます。繰り返しフィールドの各要素はタグ番号の再エンコードを必要とするため、繰り返しフィールドはこの最適化の特に良い候補となります。

フィールド値が設定されていない場合、デフォルト値が使用されます。数値型の場合はゼロ、文字列の場合は空文字列、ブール型の場合は false です。埋め込みメッセージの場合、デフォルト値は常にメッセージの「デフォルトインスタンス」または「プロトタイプ」であり、そのフィールドは何も設定されていません。明示的に設定されていないフィールドの値を取得するためにアクセサを呼び出すと、常にそのフィールドのデフォルト値が返されます。

フィールドが repeated の場合、そのフィールドは任意の回数(ゼロ回を含む)繰り返すことができます。繰り返し値の順序はプロトコルバッファ内で保持されます。繰り返しフィールドは動的にサイズ変更される配列と考えることができます。

.proto ファイルの書き方に関する完全なガイド(可能なすべてのフィールド型を含む)は、プロトコルバッファ言語ガイドにあります。ただし、クラス継承のような機能を探しても見つかりません。プロトコルバッファはそれを行いません。

プロトコルバッファのコンパイル

.proto ファイルができたら、次に行うべきは AddressBook(ひいては PersonPhoneNumber)メッセージを読み書きするために必要なクラスを生成することです。これを行うには、.proto ファイルに対してプロトコルバッファコンパイラ protoc を実行する必要があります。

  1. コンパイラをインストールしていない場合は、パッケージをダウンロードし、README の指示に従ってください。

  2. Go プロトコルバッファプラグインをインストールするには、次のコマンドを実行します。

    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
    

    コンパイラプラグイン protoc-gen-go$GOBIN にインストールされ、デフォルトでは $GOPATH/bin です。protoc コンパイラがそれを見つけるためには、$PATH に含まれている必要があります。

  3. さて、コンパイラを実行します。ソースディレクトリ(アプリケーションのソースコードがある場所。値を指定しない場合は現在のディレクトリが使用されます)、出力先ディレクトリ(生成されたコードをどこに置くか。多くの場合 $SRC_DIR と同じ)、そして .proto ファイルへのパスを指定します。この場合、次のように呼び出します。

    protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto
    

    Go コードが必要なため、--go_out オプションを使用します。他のサポートされている言語にも同様のオプションが提供されています。

これにより、指定した出力先ディレクトリに github.com/protocolbuffers/protobuf/examples/go/tutorialpb/addressbook.pb.go が生成されます。

プロトコルバッファ API

addressbook.pb.go を生成すると、以下の有用な型が得られます。

  • People フィールドを持つ AddressBook 構造体。
  • NameIdEmailPhones のフィールドを持つ Person 構造体。
  • NumberType のフィールドを持つ 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)
}

プロトコルバッファの拡張

プロトコルバッファを使用するコードをリリースした後、遅かれ早かれプロトコルバッファの定義を「改善」したくなるでしょう。新しいバッファが後方互換性があり、古いバッファが前方互換性があることを望むなら(そして、ほとんどの場合そう望むはずです)、従うべきいくつかのルールがあります。プロトコルバッファの新しいバージョンでは:

  • 既存のフィールドのタグ番号を変更してはなりません。
  • フィールドを削除することができます。
  • 新しいフィールドを追加することはできますが、新しいタグ番号(つまり、このプロトコルバッファで一度も使用されたことのないタグ番号。削除されたフィールドのものでも不可)を使用する必要があります。

(これらのルールにはいくつかの例外がありますが、それらはめったに使用されません。)

これらのルールに従えば、古いコードは新しいメッセージを問題なく読み取り、新しいフィールドは単に無視されます。古いコードにとっては、削除された単一のフィールドは単にそのデフォルト値を持つことになり、削除された繰り返しフィールドは空になります。新しいコードも、古いメッセージを透過的に読み取ります。

ただし、古いメッセージには新しいフィールドが存在しないため、デフォルト値に対して何らかの適切な処理を行う必要があることに留意してください。型固有のデフォルト値が使用されます。文字列の場合、デフォルト値は空の文字列です。ブール型の場合、デフォルト値は false です。数値型の場合、デフォルト値はゼロです。