Protocol Bufferの基本: Go

Protocol Bufferを扱うGoプログラマ向けの基本的な紹介です。

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

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

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

問題領域

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

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

  • gobsを使用してGoデータ構造をシリアライズします。これはGo固有の環境では良い解決策ですが、他のプラットフォーム用に書かれたアプリケーションとデータを共有する必要がある場合にはうまく機能しません。
  • データを単一の文字列にエンコードするアドホックな方法(例:4つのintを「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";

次に、メッセージ定義があります。メッセージは、一連の型付きフィールドを含む単なる集合体です。`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`を実行する必要があります。

  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`が生成されます。

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を使用するコードをリリースした後、遅かれ早かれProtocol Bufferの定義を「改善」したくなるでしょう。新しいバッファが後方互換性を持ち、古いバッファが前方互換性を持つことを望むなら(そしてほとんど間違いなくそうしたいでしょう)、従うべきルールがいくつかあります。Protocol Bufferの新しいバージョンでは

  • 既存のフィールドのタグ番号を変更しては*いけません*。
  • フィールドを削除しても*かまいません*。
  • 新しいフィールドを追加しても*かまいません*が、新しいタグ番号(つまり、この Protocol Buffer で一度も使用されていないタグ番号。削除されたフィールドが使用していたものも含む)を使用しなければなりません。

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

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

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