Protocol Buffers: ざっくりとした入門

# この記事について
Protocol Buffers(protobuf) の概要、基本的な使い方、*.proto ファイルの書き方についてざっくりまとめます。

- 検証環境
  - OS: Windows 10
  - 言語： Go 1.13

# Protocol Buffers とは
すでにたくさん情報があるので要点だけ記載。

- データをシリアライズ(マーシャル)するためのフォーマット、メカニズム
  - クライアント/サーバー間の通信や、データの永続化などに用いる
  - 元々は Google が開発
- IDL でメッセージ(データ構造)を定義
- 様々な言語とプラットフォームで利用可能
  - iOS や Android でも利用可能   
- バイナリ形式にシリアライズする 
  - XML よりもサイズが小さくなり、また、高速
- シリアライズ/デシリアライズ をするためのコードが自動生成される
- その他
  - 既存のプログラムに影響を与えずにメッセージを更新(フィールドの追加)が可能

## バージョン
- 最新バージョンは 3 (proto3と呼ばれる)
- proto3 と、前バージョンの proto2 は、完全には互換性が無い

## 基本的な使い方

### 1. Protocol buffers コンパイラ(protoc)をインストール

[ダウンロードサイト](https://github.com/protocolbuffers/protobuf/releases/)から自分の環境にあった protoc をダウンロード。今回は v3.11.4 の protoc-3.11.4-win64.zip をダウンロードして展開した。

注意事項：

- 展開した bin フォルダにパスを通すこと
- zip に含まれる include フォルダも展開すること(protoc が参照するファイルが入ってるため)

いちおう、バージョンの確認。

```
> protoc --version
libprotoc 3.11.4
```

### 2. 必要に応じてプラグインをインストール
Go言語の場合は protoc に対するプラグインが必要なのでインストールする。

```
> go get google.golang.org/protobuf/cmd/protoc-gen-go
> protoc-gen-go --version
protoc-gen-go v1.21.0-devel
```

なお、[ドキュメント](https://developers.google.com/protocol-buffers/docs/reference/go-generated)には `go install` を使うよう書かれてたが、自分の環境ではエラーになったので `go get` にした。

※ gRPC のスタブコードの生成については[Protocol Buffers用 Go言語APIの APIv1 と APIv2 の差異](https://qiita.com/kitauji/items/bab05cc8215abe8a6431) を参照。

### 3. *.proto ファイルを作成

`*.proto` ファイルでメッセージの定義を行う。
以下は参考用に[Go言語用のチュートリアル](https://developers.google.com/protocol-buffers/docs/gotutorial)内の addressbook.proto にコメントを入れたもの。


```protobuf:addressbook.proto

// Protocol Buffers のバージョンを指定する。省略すると proto2 と見なされる。
syntax = "proto3";

// package は、プロジェクト間での名前衝突を防ぐためのパッケージ名。
// このパッケージ名は各言語に応じた解釈が行われる。
// (例)
//   C++    : C++の名前空間になる
//   C#     : パスカル形式に変換後にC#の名前空間になる(csharp_namespaceの指定が無い場合)
//   Go     : Goのパッケージ名になる(go_packageの指定が無い場合)
//   Python : 無視される
package addresspb;

// 他の *.proto ファイルの定義された型などを読み込みたい場合は import を使う
// 以下の "google/..." は、protoc に含まれる include ディレクトリ配下を指している。
import "google/protobuf/timestamp.proto";

// option は、特定のコンテキストで解釈される。すべてのリストは以下のファイルに記載されてる。
// https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptor.proto
//
// go_package は生成される *.pb.go ファイルのパッケージを指定する。
// 無くてもコンパイルできるが警告が出る。
// なお、セミコロンを付けてパッケージのインポートパスとパッケージ名を別々に指定することも可能。
// 
// (例)
//   option go_package = "github.com/hoge/fuga";         // セミコロン無し
//   option go_package = "github.com/hoge/fuga;fuga";    // セミコロンあり
option go_package = ".;addresspb";

// メッセージ(Person)の定義
message Person {
    // メッセージのフィールド。
    // 各フィールドの識別子として 1, 2... というフィールド番号(タグ)が必要。
    // → シリアライズされたデータでは、フィールド番号でフィールドを識別するため。
    string name = 1;
    int32 id = 2;
    string email = 3;
  
    // 列挙型の定義
    enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
    }
  
    // メッセージの中に別のメッセージの定義を含められる(定義のネスト)
    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
    }
  
    // repeatedは配列(要素の数は任意。0個でもよい。)
    repeated PhoneNumber phones = 4;
  
    // Import した *.proto ファイルで定義された型
    google.protobuf.Timestamp last_updated = 5;
}

// メッセージ(AddressBook)の定義
// AddressBook は複数の Person を含む。
message AddressBook {
    repeated Person people = 1;
}
```

### 4. *.proto ファイルをコンパイル

コマンドプロンプトで `*.proto` ファイルのあるディレクトリに移動して以下を実行。今回の場合 `addressbook.pb.go` が生成される。

```
protoc addressbook.proto --go_out=.\
```

### 5. 生成されたコードを用いてシリアライズ/デシリアライズを行う
生成された `*.pb.go` には、`*.proto` で定義したメッセージ対応する struct やその Getter が定義される。


```go:addressbook.pb.go
type Person struct {
    // 中略
    Name  string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    Id    int32  `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
    Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
    Phones []*Person_PhoneNumber `protobuf:"bytes,4,rep,name=phones,proto3" json:"phones,omitempty"`
    LastUpdated *timestamp.Timestamp `protobuf:"bytes,5,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"`
}

func (x *Person) GetName() string { /* 中略 */ }
func (x *Person) GetId() int32 { /* 中略 */ } 
func (x *Person) GetEmail() string { /* 中略 */ }
func (x *Person) GetPhones() []*Person_PhoneNumber { /* 中略 */ }
func (x *Person) GetLastUpdated() *timestamp.Timestamp { /* 中略 */ }
```

これらのメッセージに対して、`github.com/golang/protobuf/proto` パッケージの Marshal(), Unmarshal() 関数を用いてシリアライズ/デシリアライズを行う。

コードはこの辺りを参照。

- [Writing a Message](https://developers.google.com/protocol-buffers/docs/gotutorial#writing-a-message)
- [Reading a Message](https://developers.google.com/protocol-buffers/docs/gotutorial#reading-a-message)

## ハマったところ

- `*.pb.go` が生成される場所  
  *.proto ファイルで `option go_package = "github.com/hoge/fuga";` というようにパッケージのフルパスを指定して protoc を実行すると、 --go_out オプションで指定したディレクトリを起点として`github.com/hoge/fuga` というディレクトリが作成されて、そこに *.pb.go が生成される。自分が生成したい場所を意識して --go_out または go_package の指定の仕方を調整する必要あり。