はじめに
gobのことを最近いろいろ調べてるんですが、そもそもなんでわざわざ言語側でメッセージ用のバイナリエンコーディング持ってるんだっけ?というのが気になったので、自分むけの整理用に使い方眺めつつこういうメリットありそうかなーとかそういうのをざっくりまとめておく
モチベとか
goblogの方に詳しそうなのでサクッとよんでみる https://go.dev/blog/gob
jsonとかprotobufもあるけどそれらでは実現できないGoに特化したものを作るのはワンチャンあるでということっぽい。
過去の失敗に学んでいい感じにしたで〜みたいな雰囲気のよう。
ゴール設定
ゴール設定としては
- Goのプログラム上の構造からサクッと利用できる
- protobufのような別のインターフェース定義や専用コンパイラを不要とする
- バイナリエンコーディングで情報の転送効率がよいこと
- streamはself-describingであること。もともとの定義などを忘れてもエンコード済みのバイナリを受け取れば元の形がわかる
- protobufで学んだいくつかの学びを取り込むこと
みたいなことが書かれている。
まあ妥当なモチベで、こういうことを達成したければ言語に特化したエンコーディングを自作するというのもたしかに選択肢に入るなという所感がある。
特に個人的に重要かなと思うのが1で、protobufもapache avroもこれは達成できてないから面白い観点だなと思った。
3についてはprotobufは部分的に満たしているけどバイナリ上はデータ型が6個とかしかなかったはずだから厳密な型の再現はできないはず。あとバイナリメッセージからはembedされてるかどうかの判断はつかないので、複雑なメッセージだとわからんとなる。apache avroは完全にスキーマ依存なので満たせてない。
既存のバイナリエンコーディングでも達成できない課題に向き合おうとしているということでほしかった理由とかは理解できた。
当時のprotoの課題感
protoでここしくじったな〜gobに活かそ!みたいなのもあった。ざっくり
- protobufはGoにおいてはstructでしか動作しない
- それ以外のメッセージもencodingできてよくね
- requiredは一見うれしいが、困ることが割とあった
- 実装がだるい
- requiredフィールドを途中から消すと、古いクライアントがぶっ壊れる(必須だと思ってるので)
- 同様にrequiredフィールドを途中から追加すると、古いクライアントがそれを送ってこないのでぶっ壊れる
- つまり前方/後方の互換を取るのが難しかった
- default設定だるい
- 値がなかったらあたかもそれを受け取ったように振る舞わねばならない
- requiredと合わせて実装がだるくなる要因
- 素直にGoの挙動(空文字ならなんなり)にあわせてよくねーか
みたいな感じ。
1についてはいまでもprotoc-gen-goを使ってコード生成するとスカラ値は ProtoMessage
的なinterfaceを実装してなくて、Unmarshal/Marshalだのに食わせられない作りなはず。サードパーティ製のツールでどうなるかわからんが、まあ素直に当時と同じくstructでしか使えないといって良い気がする。
(自分でProtoMessage実装した独自スカラ型とかは使えるかもだけど、protobufのエコシステム自体がprotocで生成されたコードを前提としてるので、スカラ値の生成コードが単体でメッセージとして利用できないならそれは利用できないといって差し支えないという認識)
2についてはproto3からrequired消えたので、それはproto3以降を使う前提ならprotobufでも解決されてる。
3についても proto3からdefault値は型によって規定された値となるよう変わった のでproto3では解決されてる。
protobufのしくじりはprotobuf側でちゃんと直されてるのが偉いっすね。とはいえgobを作ったときに感じていた課題がそこそこ解決されているということは、相対的にgobを使う意義は薄れたってことっすね。
あとの話はちょっと具体度が高いのでななめ読みで済ませた。値をこう使ってるよーとか初回コンパイル時にちょっとした仮想マシンつくっとくよーとかそういうことが書いてある。ほかには標準のrpcパッケージではgob使ってるよーとか。
現代目線からの当時のモチベへの評価
当時のモチベを現代の目線から評価すると、今でも嬉しい部分としては
- Goのプログラム上の構造からサク利用できる!struct tagすらふる必要がなく、ほかのDSLやツールチェインなど一切必要ない
- バイナリエンコーディングで情報の転送効率がよいこと(といっても仕様見る感じだと型名とかもメッセージに持つらしいのでprotobufよりは悪そう)
- streamはself-describingであること
- struct以外でも動作すること
あたりかな。protobufの失敗に学んだと言ってるところはprotobuf自身がproto3でだいたい直してしまったので現代的な目線からみると差別化要因にはならないかんじ。
gobの挙動メモ
だいたいモチベはわかったので https://pkg.go.dev/encoding/gob をみつつ利用者目線でのgobの挙動で大事そうなところメモ。
個人的なかいつまみメモなので網羅性はないです。
Each data item in the stream is preceded by a specification of its type, expressed in terms of a small set of predefined types.
とあるので、それぞれのフィールドの前に型へのメタ情報がありそう。
For structs, fields (identified by name) that are in the source but absent from the receiving variable will be ignored.
Encode/Decodeの間で利用する型は完全に一致してる必要はなくて、受信/送信ともに名前が識別子で違かったら無視されるよう。なるほどだから型名がバイナリに埋め込まれてるのかな。
ほんで名前が一致してるならそれらの型はコンパチでないとアカンらしい。exampleをみると、完全に一致してる必要はないけど一個も一致してなかったらそれはそれでエラー返すっぽい。まあそれはそうやってくれると嬉しい部分ありそう
Integers are transmitted two ways: arbitrary precision signed integers or arbitrary precision unsigned integers. There is no int8, int16 etc. discrimination in the gob format; there are only signed and unsigned integers.
符号付き整数と符号なし整数を区別するっぽいので、多分これらでバイナリ表現がことなる。これは zigzag encoding の気配を感じる
Functions and channels will not be sent in a gob. Attempting to encode such a value at the top level will fail. A struct field of chan or func type is treated exactly like an unexported field and is ignored.
chanとfuncは利用できない
あとは値を適当にencoder/decoderに突っ込むとよしなにするよみたいなことが書かれている。
まとめ
gobは他のメッセージエンコーディングと比較して
- Goのプログラム上の構造からサク利用できる!struct tagすらふる必要がなく、ほかのDSLやツールチェインなど一切必要ない
- バイナリエンコーディングで情報の転送効率がよいこと(といっても仕様見る感じだと型名とかもメッセージに持つらしいのでprotobufよりは悪そう)
- streamはself-describingであること
- struct以外でも動作すること
あたりが差別化要因となり、そのへんは現代的な観点でもメリットがありそう。
とくにツールチェインが不要なのは、学習コストなどかけずに効率良くメッセージをエンコードしたいときにかなり嬉しいはず。
streamがself-describingなのはメッセージバイナリが単体で完結してる嬉しさがある一方、他のエンコーディングと比較してバイナリ内に埋め込むメタ情報をふやさないとアカンのでトレードオフみはある。
総合的にGoでつくられたプログラム間のメッセージ伝達の媒体としてはまあ悪く無いのではという感覚。とくにGoに完結してサクッと流せるところが高評価。
肌感的には効率や使いやすさの観点でJSONとprotobufの間を埋める実装だとおもう!
ただメッセージがGoにロックインされる恐れがあるので、プロセス間通信には使っていいけど永続化するデータに使うことはあんまりおすすめしないかなぁという所感です