The Go Blog

大量のデータ

ロブ・パイク
2011年3月24日

はじめに

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

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

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

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

目標

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

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

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

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

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

プロトコルバッファの誤った機能

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

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

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

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

その結果、gobsは一種の一般化された単純化されたプロトコルバッファのように見えます。どのように機能するのでしょうか?

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

この柔軟性はポインタにも適用されます。送信前に、すべてのポインタは平坦化されます。`int8`、`*int8`、`**int8`、`****int8`などの型の値はすべて整数値として送信され、その後、任意のサイズの`int`、または`*int`、または`******int`などに格納できます。繰り返しになりますが、これにより柔軟性が得られます。

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

type T struct{ X, Y, Z int } // Only exported fields are encoded and decoded.
var t = T{X: 7, Y: 0, Z: 8}

の場合、`t`のエンコードは7と8のみを送信します。`Y`の値はゼロであるため、送信されません。ゼロ値を送信する必要はありません。

受信側は代わりにこの構造に値をデコードできます。

type U struct{ X, Y *int8 } // Note: pointers to int8s
var u U

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

整数から、他のすべての型、つまりバイト、文字列、配列、スライス、マップ、さらには浮動小数点数を構築できます。浮動小数点値は、IEEE 754浮動小数点ビットパターンとして表され、整数として格納されます。これは、型を知っていれば問題なく機能します。型は常にわかっています。ところで、その整数はバイト反転順序で送信されます。なぜなら、小さな整数など、浮動小数点数の一般的な値には、下位に多くのゼロがあり、それを送信するのを避けることができるからです。

Goが実現するgobsの優れた機能の1つは、型がGobEncoderおよびGobDecoderインターフェースを満たすようにすることで、独自のエンコーディングを定義できることです。これは、JSONパッケージのMarshalerおよびUnmarshalerと同様に、パッケージfmtStringerインターフェースにも似ています。この機能により、データを送信する際に特殊な機能を表現したり、制約を適用したり、秘密を隠したりすることが可能になります。詳細については、ドキュメントを参照してください。

ワイヤー上の型

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

したがって、最初の型`T`を送信すると、gobエンコーダは`T`の記述を送信し、それにタイプ番号、たとえば127をタグ付けします。最初のものを含むすべての値にはその番号がプレフィックスとして付けられるため、`T`値のストリームは次のようになります。

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

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

type Node struct {
    Value       int
    Left, Right *Node
}

(gobsがポインタを表さないにもかかわらず、ゼロデフォルトルールがこれをどのように機能させるかを発見するのは読者の演習です。)

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

マシンのコンパイル

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

[更新:Go 1.4以降、gobパッケージではunsafeパッケージは使用されなくなり、パフォーマンスはわずかに低下しました。]

デコードも同様ですが、より困難です。値をデコードする場合、gobパッケージは、特定のエンコーダ定義型の値を表すバイトスライスと、それをデコードするためのGo値を保持します。gobパッケージは、そのペア、つまりワイヤーで送信されたgob型とデコード用に提供されたGo型を組み合わせたマシンを構築します。ただし、そのデコードマシンが構築されると、それは再びunsafeメソッドを使用して最高速度を得るリフレクションなしのエンジンになります。

使用法

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

package main

import (
    "bytes"
    "encoding/gob"
    "fmt"
    "log"
)

type P struct {
    X, Y, Z int
    Name    string
}

type Q struct {
    X, Y *int32
    Name string
}

func main() {
    // Initialize the encoder and decoder.  Normally enc and dec would be
    // bound to network connections and the encoder and decoder would
    // run in different processes.
    var network bytes.Buffer        // Stand-in for a network connection
    enc := gob.NewEncoder(&network) // Will write to network.
    dec := gob.NewDecoder(&network) // Will read from network.
    // Encode (send) the value.
    err := enc.Encode(P{3, 4, 5, "Pythagoras"})
    if err != nil {
        log.Fatal("encode error:", err)
    }
    // Decode (receive) the value.
    var q Q
    err = dec.Decode(&q)
    if err != nil {
        log.Fatal("decode error:", err)
    }
    fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)
}

Go Playgroundでこのサンプルコードをコンパイルして実行できます。

rpcパッケージは、このエンコード/デコードの自動化をネットワーク経由のメソッド呼び出しのトランスポートに変換するためにgobsの上に構築されています。これは別の記事の主題です。

詳細

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

次の記事: Godoc: Goコードのドキュメント化
前の記事: C? Go? Cgo!
ブログインデックス