Protocol Buffersの基本的な内容に関してまとめてみた

# 初めに
業務でgrpcを使用する機会があったので、プロトコルバッファを[こちら](https://developers.google.com/protocol-buffers/docs/overview)のサイトを見ながら学習しました。
その内容をアウトプットのため投稿します。
また、使用する言語はGoでproto3を使用します。


# 概要
プロトコルバッファは、構造化されている型付けされたデータを言語やプラットフォームに依存せずシリアライズすることができます。
> 定義言語（インタフェース定義言語）・シリアライズ形式・各言語向けランタイムライブラリ・プロトコンパイラ生成コードの4要素からなる[2]。
出典:[Wikipedia](https://ja.wikipedia.org/wiki/Protocol_Buffers)

## 簡単な流れ
プロトコルバッファを使用する際の簡単な流れは以下です。
1. `.proto`ファイルにデータの定義を行う。
2. 指定した開発言語のオブジェクトを生成する。
3. バイナリファイルに変換されてデータを送受信する。

## 特徴
プロトコルバッファの特徴は以下です。
- バイナリファイルでデータの送受信がされるためjsonに比べて軽量である。
- 型安全である。
- さまざまな言語に変換可能である。
- データを追加・削除する際に既存のデータを壊すことがない。
- 数メガバイトを超えるデータには向かない。
- gRPCに使用できる。

### インタフェース定義言語のメリット
プロトコルバッファは`.proto`ファイルにデータが事前に定義されているので、フロント・モバイルアプリとバックエンド間や、マイクロサービス化されたシステムで、最初に相互の認識を合わせて開発することができます。（仕様書の様な役割を果たせる）
これはスキーマ駆動開発と呼ばれています。

# 定義方法
プロトコルバッファの`.proto`ファイルのデータ定義方法を記載します。

### syntax
まず最初にsyntaxを定義します。
冒頭にも述べましたが、今回は`proto3`を使用します。
この記述がないと`proto2`を使用するとみなされるので、`proto3`を使用したい場合は記載が必要です。

```proto
syntax = "proto3";
```

### message
messageはGoにコンパイルする際に構造体になるデータで、複数定義することができます。

#### 基本形
```proto
message メッセージの名前 {
    型 フィールド名 = フィールド番号
    型 フィールド名 = フィールド番号
}
```

実際に定義すると以下のような記載になります。

```proto
message Request {
    string s = 1;
    int32 i = 2;
}

message Response {
 ...
}
```

#### フィールド番号
フィールド番号は一意の値である必要があります。
1 ~ 15の数値は、フィールド番号とフィールドの型を含めてエンコードに1バイトかかり、16～2047のフィールド番号は2バイトかかります。
そのため、頻繁に使用するフィールドには1 ~ 15の数値を割り振った方が良いでしょう。
また、1 ~ 536,870,911までの数値を使用できますが、19000 ～ 19999は予約番号であり使用することはできません。

### コメント
コメントは`//`か`/**/`で記載可能です。
```proto
// commet
/* comment */
```
# 型
## Scalar型
`int32`、`string`、`bool`などの型があります。
[こちら](https://developers.google.com/protocol-buffers/docs/proto3#scalar)に詳細が記載してあるのでご確認ください。

## 列挙型
列挙型は、定義したいずれかのフィールドが含まれている必要があります。
詳細な定義のルールは以下です。
- フィールド名は大文字で記載する。
- 0 を数値のデフォルト値として使用できるようにするため、最初のフィールドのフィールド番号は0にする。

### 基本形

```proto
enum 名前 {
    フィールド名 = タグ番号;
    フィールド名 = タグ番号;
}
```

先ほど使用していたmessageに追加してみます。

```proto
message Request {
    string s = 1;
    int32 i = 2;
    // Scalar型と同様に扱うことができる
    em em = 3;
}
// 追加
enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}
```
`Request`メッセージに列挙型であるem型のemというフィールドを追加しました。
最初のフィールドは使用しないことが多いので`UNKNOW`というフィールドにしています。

## repeated
repeatedフィールドでは配列を扱うことができます。

```proto
repeated 型 フィールド名 = フィールド番号;
```

先ほど使用していたmessageに追加してみます。

```proto
message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    // 1つ以上のstring型の文字列が使用できる
    repeated string arr = 4;
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}
```

## map
mapではkey-valueの連想配列のような型を定義できます。
keyは`string`、`numeric`、`bool`のいずれかを使用できます。
`repeated`フィールドは使用できません。

### 基本形
```proto
map<keyの型, Valueの型> フィールド名 = フィールド番号;
```

先ほど使用していたmessageに追加してみます。

```proto
message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    repeated string arr = 4;
    // 追加
    map<string, MapValue> associative = 5;
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}
// 追加
message MapValue {}
```

keyがstring型でvalueが`MapValue`messageのmapを作成することができました。

## Oneof
Oneofは定義した複数の型のいずれかがを値として持ちます。

```proto
oneof フィールド名 {
        型 フィールド名 = フィールド番号;
        型 フィールド名 = フィールド番号;
}
```

先ほど使用していたmessageに追加してみます。

```proto
message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    repeated string arr = 4;
    map<string, MapValue> associative = 5;
    // 追加
    oneof O {
        string OS = 6;
        OneofField OF = 7;
    }
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}

message MapValue {}
// 追加
message OneofField {}
```
Oフォールドはstring型、`OneofField`型のいずれかを受け入れます。

## ゼロ値
定義しているフィールドに値が格納されていない場合に、型にはゼロ値が格納されます。
各型のゼロ値は以下の表を参照ください。

| 型 | ゼロ値 |
| ---| --- |
| strings|空の文字列 |
| bytes|空のバイト |
|bools | false|
| numeric| 0|
|enums |最初に定義された列挙値で、0 にする必要がある |
| repeated | 空のリスト |

> 参照: https://developers.google.com/protocol-buffers/docs/proto3#default

## messageのネスト
messageはネストすることができます。
ネストされたmessageは、`.`を用いてアクセスすることができます。
ネストされた子のmessageのスコープは親のmessage内であるため、親のmessage外でも別のmessageとして定義することができます。

```proto
message A {
    message B {}
}
// 上記のBというmessageとは別のmessageである
message B {}
// Bへアクセス
A.B
```

先ほど使用していたmessageに追加してみます。

```proto
message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    repeated string arr = 4;
    map<string, MapValue> associative = 5;
    oneof O {
        string OS = 6;
        OneofField OF = 7;
    }
    // 追加
    Parent.Cheidlen nest = 8;
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}

message MapValue {}
message OneofField {}
// 追加
message Parent {
    message Cheidlen {}
}
```

## import
プロトコルバッファでは、別のファイルのprotoファイルをimportして使用することができます。

```proto
import "パス.proto";
```

先ほどのファイルに別のファイルを追加してみます。

ディレクトリ構成は以下の様になります。

```
proto
├── another.proto
└── file.proto
```
`file.proto`が今までの使用していたファイルで`another.proto`が新しく追加したファイルです。
カレントディレクトリは`proto`ディレクトリです。

では、`another.proto`にデータの定義をしていきます。

```proto
syntax = "proto3";

message Other {
    int32 i = 1;
}
```
`Other`というmessageを定義しました。
上記を`file.proto`で読み込んで使用します。

```proto
syntax = "proto3";

import "another.proto";

message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    repeated string arr = 4;
    map<string, MapValue> associative = 5;
    oneof O {
        string OS = 6;
        OneofField OF = 7;
    }
    Parent.Cheidlen nest = 8;
    // 追加
    Other another = 9;
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}

message MapValue {}
message OneofField {}
message Parent {
    message Cheidlen {}
}
message Cheidlen {}
```

`another`という`another.proto`のフィールドを`file.proto`で読み込みました。

#### プロトコンパイルする際
実際にプロトコンパイルする際は、`-I / --proto_path`オプションを使用してディレクトリを指定する必要があります。
今回は、カレントディレクトリである`proto`ディレクトリを指定することで、正常にコンパイルすることができます。

## package
packageを使用することで、各ファイルに名前競合を防ぐことができます。
同じ名称のmessageがある場合などに有用です。

```proto
package パッケージ名;
```
実際に`file.proto`と`another.proto`にパッケージ名を追加してみます。
`file.proto`
```proto
syntax = "proto3";
// 追加
package file;

import "another.proto";

message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    repeated string arr = 4;
    map<string, MapValue> associative = 5;
    oneof O {
        string OS = 6;
        OneofField OF = 7;
    }
    Parent.Cheidlen nest = 8;
    // 更新
    another.Other another = 9;
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}

message MapValue {}
message OneofField {}
message Parent {
    message Cheidlen {}
}
message Cheidlen {}
```


`another.proto`
```proto
syntax = "proto3";
// 追加
package another;

message Other {
    int32 i = 1;
}
```

今回は名前の競合が起きていないので付与する意味はありませんが、実装方法を残すため定義してみました。

## option
optionを使用して、コンパイルされるGoファイルのpackageを指定します。

`option go_package = "./pb";`という記述を`file.proto`、`another.proto`の両方に記載します。

`file.proto`
```proto
syntax = "proto3";
// 追加
option go_package = "./pb";

package file;

import "another.proto";

message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    repeated string arr = 4;
    map<string, MapValue> associative = 5;
    oneof O {
        string OS = 6;
        OneofField OF = 7;
    }
    Parent.Cheidlen nest = 8;
    another.Other another = 9;
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}

message MapValue {}
message OneofField {}
message Parent {
    message Cheidlen {}
}
message Cheidlen {}
```

`another.proto`
```proto
syntax = "proto3";
// 追加
package another;

option go_package = "./pb";

message Other {
    int32 i = 1;
}
```

## プロトコンパイル
最後に、下記コマンドを使用して今まで記載してきたprotoファイルをGoへコンパイルします。

```terminal
protoc -I. --go_out=. *.proto
```

- `-I.`はprotoファイルの`import`文のパスを特定しています。
    - 今回はカレントディレクトリである`proto`ディレクトリ内で作業しているので`.`を指定しています。
- `--go_out=.`は、Goへ変換するコマンドです。`.`は出力されるファイルの場所を指定しています。
- `*.proto`今回は、カレントディレクトリ配下の2つをコンパイルするので、全ての.protoの拡張子のファイルをコンパイルする様に設定しています。

### 実際に変換した後

```
├── another.proto
├── file.proto
└── pb
    ├── another.pb.go
    └── file.pb.go
```

実際に変換すると上記のようなディレクトリ構造になり、`another.pb.go`、`file.pb.go`ファイルが生成されています。

ファイルの中に定義したmessageが構造体として定義されていることが分かります。

```pb/file.pb.go
type Request struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	S           string               `protobuf:"bytes,1,opt,name=s,proto3" json:"s,omitempty"`
	I           int32                `protobuf:"varint,2,opt,name=i,proto3" json:"i,omitempty"`
	Em          Em                   `protobuf:"varint,3,opt,name=em,proto3,enum=file.Em" json:"em,omitempty"`
	Arr         []string             `protobuf:"bytes,4,rep,name=arr,proto3" json:"arr,omitempty"`
	Associative map[string]*MapValue `protobuf:"bytes,5,rep,name=associative,proto3" json:"associative,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
	// Types that are assignable to O:
	//
	//	*Request_OS
	//	*Request_OF
	O       isRequest_O      `protobuf_oneof:"O"`
	Nest    *Parent_Cheidlen `protobuf:"bytes,8,opt,name=nest,proto3" json:"nest,omitempty"`
	Another *Other           `protobuf:"bytes,9,opt,name=another,proto3" json:"another,omitempty"`
}
```

```pb/another.pb.go
type Other struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	I int32 `protobuf:"varint,1,opt,name=i,proto3" json:"i,omitempty"`
}
```

以上。


## 参照サイト
[Protocol Buffers ドキュメント](https://developers.google.com/protocol-buffers/docs/reference/overview)
[gRPC Golang](https://grpc.io/docs/languages/go/quickstart/)