Goブログ

大量のデータ

Rob Pike
2011年3月24日

はじめに

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

新しいエンコードを定義する理由は何でしょうか?それは多くの労力と冗長性があります。既存のフォーマットの1つを使えばいいのではないでしょうか?ええと、一つには、私たちはそうしています!Goには、上記で言及したすべてのエンコードをサポートするパッケージがあります(プロトコルバッファパッケージは別のリポジトリにありますが、最も頻繁にダウンロードされるパッケージの1つです)。他の言語で書かれたツールやシステムとの通信など、多くの目的のために、それらは正しい選択です。

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

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

目標

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

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

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

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

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

プロトコルバッファの欠陥

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

まず、プロトコルバッファは、Goでstructと呼ぶデータ型でのみ機能します。最上位レベルで整数や配列をエンコードすることはできません。その中にフィールドを持つstructのみが可能です。少なくともGoでは、それは無意味な制限のように思えます。送信したいものが整数の配列だけの場合、なぜ最初にstructに入れる必要があるのでしょうか?

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

3番目のプロトコルバッファの欠陥は、デフォルト値です。プロトコルバッファが「デフォルト設定」フィールドの値を省略した場合、デコードされた構造は、フィールドがその値に設定されているかのように動作します。このアイデアは、フィールドへのアクセスを制御するためのgetterメソッドとsetterメソッドがある場合にはうまく機能しますが、コンテナーが単なるプレーンな慣用的なstructの場合には、クリーンに処理するのが困難です。必須フィールドも実装が難しくなります。デフォルト値をどこで定義するのか、それらはどのようなタイプを持っているのか(テキストはUTF-8ですか?解釈されていないバイトですか?浮動小数点数には何ビットありますか?)、そして見かけの単純さにもかかわらず、プロトコルバッファの設計と実装には多くの複雑さがありました。gobsからそれらを削除し、Goの単純だが効果的なデフォルト規則に戻すことにしました。他に何も設定しない限り、そのタイプの「ゼロ値」になり、送信する必要はありません。

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

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

この柔軟性はポインタにも適用されます。送信前に、すべてのポインタが平坦化されます。タイプint8*int8**int8****int8などの値はすべて整数値として送信され、任意のサイズのint、または*int、または******intなどに保存できます。繰り返しになりますが、これにより柔軟性が向上します。

柔軟性は、structをデコードするときに、エンコーダーによって送信されたフィールドのみが宛先に保存されるためにも発生します。次の値が与えられた場合

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

整数から、バイト、文字列、配列、スライス、マップ、さらには浮動小数点数まですべての他のタイプを構築できます。浮動小数点値は、整数として格納されたIEEE 754浮動小数点ビットパターンで表されます。これは、タイプがわかっている限り(常に知っています)、正常に機能します。ちなみに、浮動小数点数の一般的な値(小さな整数など)は、送信を回避できるローエンドに多くのゼロがあるため、その整数はバイト反転順序で送信されます。

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

回線上のタイプ

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

したがって、最初のタイプ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!
ブログインデックス