Introduction

データ構造をネットワーク越しに送信したり、ファイルに保存したりするためには、エンコードされ、その後再度デコードされる必要があります。もちろん、利用可能なエンコーディングは多数あります: JSONXML、Googleのプロトコルバッファ、その他もあります。そして今、Goのgobパッケージによって提供される別のものがあります。

新しいエンコーディングを定義する理由は何でしょうか?それは多くの作業であり、冗長です。既存のフォーマットの1つを使用すればよいのではないでしょうか?まあ、1つの理由は、私たちはそうしています!Goには、先に述べたすべてのエンコーディングをサポートするパッケージがあります(プロトコルバッファパッケージは別のリポジトリにありますが、最も頻繁にダウンロードされるものの1つです)。そして、他の言語で書かれたツールやシステムとの通信を含む多くの目的において、それらは適切な選択です。

しかし、Goで書かれた2つのサーバー間の通信のようなGo特有の環境では、使いやすく、効率的なものを構築する機会があります。

Gobsは、外部で定義された言語非依存のエンコーディングではできない方法で言語と連携します。同時に、既存のシステムから学ぶべき教訓もあります。

Goals

gobパッケージは、いくつかの目標を念頭に置いて設計されました。

まず、最も明白なことは、非常に使いやすくなければならないということです。まず、Goにはリフレクションがあるため、別のインターフェース定義言語や「プロトコルコンパイラ」は必要ありません。データ構造自体が、パッケージがそれをエンコードおよびデコードする方法を理解するために必要なすべてです。一方、このアプローチは、gobsが他の言語と同じようにうまく機能することは決してないことを意味しますが、それは問題ありません:gobsは恥ずかしげもなくGo中心です。

効率も重要です。XMLやJSONに例示されるテキスト表現は、効率的な通信ネットワークの中心に置くには遅すぎます。バイナリエンコーディングが必要です。

Gobストリームは自己記述的でなければなりません。最初から読み取った各gobストリームには、その内容について事前に何も知らないエージェントがストリーム全体を解析できるのに十分な情報が含まれています。この特性により、ファイルに保存されたgobストリームをデコードすることが常に可能になります。たとえそのデータが何を表しているのかを忘れてしまったとしてもです。

Googleプロトコルバッファとの経験から学ぶべきこともいくつかありました。

Protocol buffer misfeatures

プロトコルバッファはgobsの設計に大きな影響を与えましたが、意図的に避けられた3つの特徴があります。(プロトコルバッファが自己記述的でないという特性は別として:プロトコルバッファをエンコードするために使用されるデータ定義を知らない場合、解析できないかもしれません。)

まず、プロトコルバッファはGoで構造体と呼ばれるデータ型でのみ機能します。整数や配列をトップレベルでエンコードすることはできず、内部にフィールドを持つ構造体のみがエンコードできます。それは、少なくともGoにおいては無意味な制限のように思えます。整数の配列を送信したいだけなら、なぜ最初にそれを構造体に入れなければならないのでしょうか?

次に、プロトコルバッファの定義は、T.xおよびT.yフィールドがT型の値がエンコードまたはデコードされるときに存在する必要があることを指定する場合があります。このような必須フィールドは良いアイデアのように思えるかもしれませんが、コーデックはエンコードおよびデコード中に別のデータ構造を維持しなければならないため、実装コストが高くなります。これは、必須フィールドが欠落していると報告できるようにするためです。また、メンテナンスの問題にもなります。時間が経つにつれて、必須フィールドを削除するためにデータ定義を変更したい場合がありますが、それはデータの既存のクライアントをクラッシュさせる可能性があります。エンコーディングにそれらを含めない方が良いです。(プロトコルバッファにはオプションフィールドもあります。しかし、必須フィールドがない場合、すべてのフィールドはオプションであり、それがすべてです。オプションフィールドについては、もう少し後で詳しく説明します。)

プロトコルバッファの3つ目の欠点はデフォルト値です。プロトコルバッファが「デフォルト化」されたフィールドの値を省略すると、デコードされた構造体はそのフィールドがその値に設定されているかのように振る舞います。このアイデアは、フィールドへのアクセスを制御するためのゲッターおよびセッター メソッドがある場合にはうまく機能しますが、コンテナが単なる平易な慣用的構造体である場合には、クリーンに処理するのが難しくなります。必須フィールドの実装も難しいです:デフォルト値はどこで定義されるのか、どのような型を持つのか(テキストはUTF-8ですか?解釈されていないバイトですか?浮動小数点数は何ビットですか?)そして、見かけ上の単純さにもかかわらず、プロトコルバッファの設計と実装にはいくつかの複雑さがありました。私たちはそれらをgobsから除外し、Goの単純だが効果的なデフォルトルールに戻ることにしました:何かを別の方法で設定しない限り、それはその型の「ゼロ値」を持ち、送信する必要はありません。

したがって、gobsは一種の一般化された、簡略化されたプロトコルバッファのように見えます。どのように機能するのでしょうか?

Values

エンコードされたgobデータは、int8uint16のような型に関するものではありません。代わりに、Goの定数に類似して、その整数値は抽象的でサイズのない数値であり、符号付きまたは符号なしです。int8をエンコードすると、その値はサイズのない可変長整数として送信されます。int64をエンコードすると、その値もサイズのない可変長整数として送信されます。(符号付きと符号なしは明確に扱われますが、同じサイズのない性質は符号なしの値にも適用されます。)両方の値が7の場合、送信されるビットは同一になります。受信者がその値をデコードすると、それは受信者の変数に格納されますが、その変数は任意の整数型である可能性があります。したがって、エンコーダはint8から来た7を送信できますが、受信者はそれをint64に格納することができます。これは問題ありません:その値は整数であり、収まる限り、すべてが機能します。(収まらない場合はエラーが発生します。)変数のサイズからのこのデカップリングは、エンコーディングに柔軟性を与えます:ソフトウェアが進化するにつれて整数変数の型を拡張できますが、古いデータをデコードすることもできます。

この柔軟性はポインタにも適用されます。送信前に、すべてのポインタはフラット化されます。int8*int8**int8****int8などの型の値はすべて整数値として送信され、その後intの任意のサイズに格納されるか、*int******intなどに格納されます。これにより、柔軟性が得られます。

柔軟性は、構造体をデコードする際にも発生します。エンコーダによって送信されたフィールドのみが宛先に格納されます。値が

  1. type T struct{ X, Y, Z int } // Only exported fields are encoded and decoded.
  2. var t = T{X: 7, Y: 0, Z: 8}
  1. 受信者は代わりにこの構造体にデコードできます:
  2. ``````bash
  3. type U struct{ X, Y *int8 } // Note: pointers to int8s
  4. var u U
  5. `

uの値を取得し、Xのみが設定されます(int8変数のアドレスに設定され、7に設定されます);Zフィールドは無視されます - どこに置くのでしょうか?構造体をデコードする際、フィールドは名前と互換性のある型で一致し、両方に存在するフィールドのみが影響を受けます。このシンプルなアプローチは「オプションフィールド」問題を巧みに解決します:T型がフィールドを追加することで進化するにつれて、古い受信者は認識できる型の部分で機能し続けます。したがって、gobsはオプションフィールドの重要な結果 - 拡張性 - を提供しますが、追加のメカニズムや表記は必要ありません。

整数から、すべての他の型を構築できます:バイト、文字列、配列、スライス、マップ、さらには浮動小数点数。浮動小数点値は、整数として保存されたIEEE 754浮動小数点ビットパターンで表され、型を知っている限り(常に知っています)、うまく機能します。ちなみに、その整数はバイト逆順で送信されます。なぜなら、小さな整数などの浮動小数点数の一般的な値は、低い部分に多くのゼロがあるため、送信を避けることができるからです。

gobsの1つの素晴らしい機能は、Goが可能にするもので、あなた自身のエンコーディングを定義できることです。あなたの型がゴブエンコーダおよびゴブデコーダインターフェースを満たすことで、JSONパッケージのマシャラーおよびアンマシャラーと同様に、fmtパッケージStringerインターフェースにも似ています。この機能により、特別な機能を表現したり、制約を強制したり、データを送信する際に秘密を隠したりすることが可能になります。詳細については、ドキュメントを参照してください。

Types on the wire

特定の型を初めて送信する際、gobパッケージはデータストリームにその型の説明を含めます。実際に何が起こるかというと、エンコーダはその型を説明し、ユニークな番号を付けた内部構造体を標準gobエンコーディング形式でエンコードするために使用されます。(基本型と型説明構造体のレイアウトは、ブートストラップ用にソフトウェアによって事前定義されています。)型が説明された後、それはその型番号で参照できます。

したがって、最初の型Tを送信すると、gobエンコーダはTの説明を送信し、それに型番号を付けます。たとえば127です。すべての値、最初の値を含めて、その番号でプレフィックスされるため、T値のストリームは次のようになります:

  1. ("define type id" 127, definition of type T)(127, T value)(127, T value), ...

これらの型番号により、再帰型を説明し、それらの型の値を送信することが可能になります。したがって、gobsは木のような型をエンコードできます:

  1. type Node struct {
  2. Value int
  3. Left, Right *Node
  4. }

(ゼロデフォルトルールがどのように機能するかを発見するのは読者の課題です。gobsはポインタを表現しないにもかかわらず。)

型情報があれば、gobストリームはブートストラップ型のセットを除いて完全に自己記述的です。これは明確に定義された出発点です。

Compiling a machine

特定の型の値を初めてエンコードすると、gobパッケージはそのデータ型に特有の小さな解釈機械を構築します。それはその型に対してリフレクションを使用してその機械を構築しますが、一度機械が構築されると、リフレクションには依存しません。その機械は、パッケージunsafeといくつかのトリックを使用して、データを高速度でエンコードされたバイトに変換します。リフレクションを使用してunsafeを回避することもできますが、かなり遅くなります。(Goのプロトコルバッファサポートも同様の高速アプローチを採用しており、その設計はgobsの実装に影響を受けました。)同じ型の後続の値は、すでにコンパイルされた機械を使用するため、すぐにエンコードできます。

[更新:Go 1.4以降、gobパッケージはもはやunsafeパッケージを使用しておらず、わずかなパフォーマンス低下があります。]

デコードは似ていますが、より難しいです。値をデコードするとき、gobパッケージはデコードするためのエンコーダ定義型の値を表すバイトスライスと、それをデコードするためのGo値を保持します。gobパッケージは、そのペアのための機械を構築します:送信されたgob型とデコード用に提供されたGo型が交差します。そのデコード機械が構築されると、それは再びリフレクションなしのエンジンであり、最大速度を得るためにunsafeメソッドを使用します。

Use

内部では多くのことが行われていますが、その結果はデータを送信するための効率的で使いやすいエンコーディングシステムです。異なるエンコードされた型とデコードされた型を示す完全な例を以下に示します。値を送信し受信するのがどれほど簡単かに注目してください。必要なのは、値と変数をgobパッケージに提示するだけで、すべての作業を行います。

  1. package main
  2. import (
  3. "bytes"
  4. "encoding/gob"
  5. "fmt"
  6. "log"
  7. )
  8. type P struct {
  9. X, Y, Z int
  10. Name string
  11. }
  12. type Q struct {
  13. X, Y *int32
  14. Name string
  15. }
  16. func main() {
  17. // Initialize the encoder and decoder. Normally enc and dec would be
  18. // bound to network connections and the encoder and decoder would
  19. // run in different processes.
  20. var network bytes.Buffer // Stand-in for a network connection
  21. enc := gob.NewEncoder(&network) // Will write to network.
  22. dec := gob.NewDecoder(&network) // Will read from network.
  23. // Encode (send) the value.
  24. err := enc.Encode(P{3, 4, 5, "Pythagoras"})
  25. if err != nil {
  26. log.Fatal("encode error:", err)
  27. }
  28. // Decode (receive) the value.
  29. var q Q
  30. err = dec.Decode(&q)
  31. if err != nil {
  32. log.Fatal("decode error:", err)
  33. }
  34. fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)
  35. }

この例のコードは、Go Playgroundでコンパイルして実行できます。

rpcパッケージはgobsを基にして、エンコード/デコードの自動化をネットワーク越しのメソッド呼び出しの輸送に変換します。それは別の記事の主題です。

Details

gobパッケージのドキュメント、特にファイルdoc.goは、ここで説明されている多くの詳細を拡張し、エンコーディングがデータをどのように表現するかを示す完全な作業例を含んでいます。gob実装の内部に興味がある場合は、そこから始めるのが良い場所です。