Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10716] ファイルの概要

このコミットは、Go言語の標準ライブラリであるtimeパッケージに、time.Time型のGobエンコーディングおよびデコーディング機能を追加するものです。これにより、time.Time型の値をGoのgobパッケージを使用してシリアライズおよびデシリアライズできるようになります。

コミット

commit d0cf3fa21ed7017eafa05f2e612c0b8f5cdcd20d
Author: Robert Hencke <robert.hencke@gmail.com>
Date:   Mon Dec 12 16:08:29 2011 -0500

    time: gob marshaler for Time
    
    Addresses issue 2526
    
    R=rsc, r
    CC=golang-dev
    https://golang.org/cl/5448114

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d0cf3fa21ed7017eafa05f2e612c0b8f5cdcd20d

元コミット内容

time: gob marshaler for Time

このコミットは、time.Time型にGobマーシャリング機能を追加します。

変更の背景

この変更は、GoのIssue 2526に対応するものです。Goのgobパッケージは、Goのデータ構造をバイナリ形式でエンコードおよびデコードするためのメカニズムを提供します。これは、ネットワーク経由でのデータ転送や、ファイルへの永続化など、異なるGoプログラム間でデータをやり取りする際に非常に便利です。

しかし、コミット時点では、time.Time型は標準でgobパッケージによるエンコード/デコードをサポートしていませんでした。そのため、time.Time型のデータを含む構造体をgobで処理しようとすると、エラーが発生するか、期待通りの動作をしない可能性がありました。このコミットは、time.Time型がgobパッケージのインターフェース(GobEncoderGobDecoder)を実装することで、この問題を解決し、time.Time型のシリアライズを可能にすることを目的としています。

これにより、開発者はtime.Time型を他のGoのデータ型と同様にgobで簡単に扱うことができるようになり、Goアプリケーション間での日付と時刻のデータの交換がよりスムーズになります。

前提知識の解説

Goのgobパッケージ

gobパッケージは、Goのデータ構造をバイナリ形式でエンコード(シリアライズ)およびデコード(デシリアライズ)するためのGo標準ライブラリです。これは、Goプログラム間でデータを効率的に転送したり、永続化したりするのに役立ちます。gobは、データ型を自己記述的にエンコードするため、受信側は送信側と同じ型定義を持っている必要がありません(ただし、互換性のある型である必要があります)。

gobパッケージでカスタム型をエンコード/デコード可能にするには、その型がGobEncoderおよびGobDecoderインターフェースを実装する必要があります。

  • GobEncoderインターフェース:

    type GobEncoder interface {
        GobEncode() ([]byte, error)
    }
    

    このメソッドは、型をバイトスライスにエンコードするロジックを実装します。

  • GobDecoderインターフェース:

    type GobDecoder interface {
        GobDecode([]byte) error
    }
    

    このメソッドは、バイトスライスから型をデコードするロジックを実装します。

time.Time

time.Time型は、Go言語で日付と時刻を扱うための構造体です。特定の時点(UTCからの経過秒数とナノ秒数)と、その時刻がどのタイムゾーン(ロケーション)に属するかという情報を持っています。タイムゾーン情報は、時刻の表示形式や、夏時間などのルールに影響を与えます。

time.Timeの内部構造は、主に以下のフィールドで構成されます(実装の詳細により異なる場合がありますが、概念的には):

  • sec: 1970年1月1日UTCからの経過秒数(Unixエポック秒)。
  • nsec: 秒未満のナノ秒。
  • loc: タイムゾーン情報(*time.Location型)。

time.Timeの重要な特性として、そのロケーション情報が挙げられます。gobでシリアライズする際には、このロケーション情報も正確に保存・復元する必要があります。特に、UTC、ローカルタイム、および固定オフセットのタイムゾーンを区別して扱う必要があります。

技術的詳細

このコミットでは、time.Time型がgobパッケージで適切に扱われるように、GobEncodeGobDecodeの2つのメソッドがtime.Time型に追加されています。

GobEncodeの実装 (time.Time.GobEncode)

  1. バージョンバイトの導入: エンコードされたデータの最初のバイトとしてtimeGobVersion (値は1) が導入されています。これは、将来的にエンコーディング形式が変更された場合に、後方互換性を維持するためのバージョン管理メカニズムです。
  2. タイムゾーンオフセットの処理:
    • t.Location()がUTC (&utcLoc) の場合、offsetMin-1に設定されます。これは、UTCを特別な値として識別するためです。
    • それ以外の場合、t.Zone()から現在のタイムゾーンのオフセット(秒単位)を取得し、それを分単位に変換します。
    • オフセットが分数の端数を持つ場合(例: 30秒のオフセット)や、int16の範囲外である場合、または-1(UTCの識別子と衝突するため)である場合はエラーを返します。これは、gobエンコーディングでタイムゾーンオフセットをint16で表現するための制約です。
  3. データのエンコード:
    • timeGobVersion (1バイト)
    • t.sec (int64): 8バイトでエンコードされます。これは、Unixエポックからの秒数を表します。
    • t.nsec (int32): 4バイトでエンコードされます。これは、秒未満のナノ秒を表します。
    • offsetMin (int16): 2バイトでエンコードされます。これは、UTCからの分単位のオフセットを表します。 これらの値は、ビッグエンディアン形式でバイトスライスに格納されます。

GobDecodeの実装 (*time.Time.GobDecode)

  1. 入力検証:
    • 入力バイトスライスbufが空の場合、エラーを返します。
    • 最初のバイト(バージョンバイト)がtimeGobVersionと一致しない場合、サポートされていないバージョンとしてエラーを返します。
    • バイトスライスの長さが期待される長さ(バージョン1バイト + 秒8バイト + ナノ秒4バイト + ゾーンオフセット2バイト = 15バイト)と異なる場合、無効な長さとしてエラーを返します。
  2. データのデコード:
    • bufからt.sect.nsec、およびoffset(分単位)をデコードします。エンコード時と同様に、ビッグエンディアン形式でバイトスライスから値を読み取ります。
  3. タイムゾーンの復元:
    • デコードされたoffset-1 * 60(つまり、offsetMin-1だった場合)であれば、ロケーションをUTC (&utcLoc) に設定します。
    • それ以外の場合、デコードされたオフセットが現在のシステムのローカルタイムゾーンのオフセットと一致するかどうかをチェックします。一致すれば、ロケーションをLocalに設定します。
    • 上記いずれにも該当しない場合、デコードされたオフセットに基づいてFixedZoneを作成し、ロケーションを設定します。これにより、元のタイムゾーン情報が正確に復元されます。

エラーハンドリング

gobErrorというカスタムエラー型が定義されており、エンコード/デコード中に発生する特定のエラー(例: 分数オフセット、範囲外のオフセット、無効なデータ形式)を報告するために使用されます。

テストの追加 (src/pkg/time/time_test.go)

新しいGobEncodeGobDecodeメソッドの正確性を検証するために、包括的なテストケースが追加されています。

  • gobTests: さまざまなtime.Time値(UTC、固定オフセット、nilロケーション、特定のUnix秒とナノ秒を持つ時間)を含むテストデータセット。
  • TestTimeGob: gobTestsの各時間値をエンコードし、その後デコードし、元の値とデコードされた値が等しいことを検証します。特に、Equalメソッドだけでなく、タイムゾーンの名前とオフセットも比較して、ロケーション情報が正しく復元されていることを確認します。
  • invalidEncodingTests: 無効なバイトスライス(空、不正なバージョン、不正な長さ)をGobDecodeに渡し、期待されるエラーが返されることを検証します。
  • notEncodableTimes: GobEncodeでエンコードできないはずのtime.Time値(例: 分数オフセットを持つタイムゾーン、int16の範囲外のオフセット)を渡し、期待されるエラーが返されることを検証します。

これらのテストは、gobマーシャリング機能が堅牢であり、さまざまなエッジケースやエラーシナリオを適切に処理できることを保証します。

コアとなるコードの変更箇所

src/pkg/time/time.go

// time.go の変更点
+type gobError string
+
+func (g gobError) Error() string { return string(g) }
+
+const timeGobVersion byte = 1
+
+// GobEncode implements the gob.GobEncoder interface.
+func (t Time) GobEncode() ([]byte, error) {
+	var offsetMin int16 // minutes east of UTC. -1 is UTC.
+
+	if t.Location() == &utcLoc {
+		offsetMin = -1
+	} else {
+		_, offset := t.Zone()
+		if offset%60 != 0 {
+			return nil, gobError("Time.GobEncode: zone offset has fractional minute")
+		}
+		offset /= 60
+		if offset < -32768 || offset == -1 || offset > 32767 {
+			return nil, gobError("Time.GobEncode: unexpected zone offset")
+		}
+		offsetMin = int16(offset)
+	}
+
+	enc := []byte{
+		timeGobVersion,    // byte 0 : version
+		byte(t.sec >> 56), // bytes 1-8: seconds
+		byte(t.sec >> 48),
+		byte(t.sec >> 40),
+		byte(t.sec >> 32),
+		byte(t.sec >> 24),
+		byte(t.sec >> 16),
+		byte(t.sec >> 8),
+		byte(t.sec),
+		byte(t.nsec >> 24), // bytes 9-12: nanoseconds
+		byte(t.nsec >> 16),
+		byte(t.nsec >> 8),
+		byte(t.nsec),
+		byte(offsetMin >> 8), // bytes 13-14: zone offset in minutes
+		byte(offsetMin),
+	}
+
+	return enc, nil
+}
+
+// GobDecode implements the gob.GobDecoder interface.
+func (t *Time) GobDecode(buf []byte) error {
+	if len(buf) == 0 {
+		return gobError("Time.GobDecode: no data")
+	}
+
+	if buf[0] != timeGobVersion {
+		return gobError("Time.GobDecode: unsupported version")
+	}
+
+	if len(buf) != /*version*/ 1+ /*sec*/ 8+ /*nsec*/ 4+ /*zone offset*/ 2 {
+		return gobError("Time.GobDecode: invalid length")
+	}
+
+	buf = buf[1:]
+	t.sec = int64(buf[7]) | int64(buf[6])<<8 | int64(buf[5])<<16 | int64(buf[4])<<24 |
+		int64(buf[3])<<32 | int64(buf[2])<<40 | int64(buf[1])<<48 | int64(buf[0])<<56
+
+	buf = buf[8:]
+	t.nsec = int32(buf[3]) | int32(buf[2])<<8 | int32(buf[1])<<16 | int32(buf[0])<<24
+
+	buf = buf[4:]
+	offset := int(int16(buf[1])|int16(buf[0])<<8) * 60
+
+	if offset == -1*60 {
+		t.loc = &utcLoc
+	} else if _, localoff, _, _, _ := Local.lookup(t.sec + internalToUnix); offset == localoff {
+		t.loc = Local
+	} else {
+		t.loc = FixedZone("", offset)
+	}
+
+	return nil
+}

src/pkg/time/time_test.go

// time_test.go の変更点
 import (
+	"bytes"
+	"encoding/gob"
 	"strconv"
 	"strings"
 	"testing"
@@ -666,6 +668,74 @@ func TestAddToExactSecond(t *testing.T) {
 	}
 }
 
+var gobTests = []Time{
+	Date(0, 1, 2, 3, 4, 5, 6, UTC),
+	Date(7, 8, 9, 10, 11, 12, 13, FixedZone("", 0)),
+	Unix(81985467080890095, 0x76543210), // Time.sec: 0x0123456789ABCDEF
+	Time{},                              // nil location
+	Date(1, 2, 3, 4, 5, 6, 7, FixedZone("", 32767*60)),
+	Date(1, 2, 3, 4, 5, 6, 7, FixedZone("", -32768*60)),
+}
+
+func TestTimeGob(t *testing.T) {
+	var b bytes.Buffer
+	enc := gob.NewEncoder(&b)
+	dec := gob.NewDecoder(&b)
+	for _, tt := range gobTests {
+		var gobtt Time
+		if err := enc.Encode(&tt); err != nil {
+			t.Errorf("%v gob Encode error = %q, want nil", tt, err)
+		} else if err := dec.Decode(&gobtt); err != nil {
+			t.Errorf("%v gob Decode error = %q, want nil", tt, err)
+		} else {
+			gobname, goboffset := gobtt.Zone()
+			name, offset := tt.Zone()
+			if !gobtt.Equal(tt) || goboffset != offset || gobname != name {
+				t.Errorf("Decoded time = %v, want %v", gobtt, tt)
+			}
+		}
+		b.Reset()
+	}
+}
+
+var invalidEncodingTests = []struct {
+	bytes []byte
+	want  string
+}{
+	{[]byte{}, "Time.GobDecode: no data"},
+	{[]byte{0, 2, 3}, "Time.GobDecode: unsupported version"},
+	{[]byte{1, 2, 3}, "Time.GobDecode: invalid length"},
+}
+
+func TestInvalidTimeGob(t *testing.T) {
+	for _, tt := range invalidEncodingTests {
+		var ignored Time
+		err := ignored.GobDecode(tt.bytes)
+		if err == nil || err.Error() != tt.want {
+			t.Errorf("time.GobDecode(%#v) error = %v, want %v", tt.bytes, err, tt.want)
+		}
+	}
+}
+
+var notEncodableTimes = []struct {
+	time Time
+	want string
+}{
+	{Date(0, 1, 2, 3, 4, 5, 6, FixedZone("", 1)), "Time.GobEncode: zone offset has fractional minute"},
+	{Date(0, 1, 2, 3, 4, 5, 6, FixedZone("", -1*60)), "Time.GobEncode: unexpected zone offset"},
+	{Date(0, 1, 2, 3, 4, 5, 6, FixedZone("", -32769*60)), "Time.GobEncode: unexpected zone offset"},
+	{Date(0, 1, 2, 3, 4, 5, 6, FixedZone("", 32768*60)), "Time.GobEncode: unexpected zone offset"},
+}
+
+func TestNotGobEncodableTime(t *testing.T) {
+	for _, tt := range notEncodableTimes {
+		_, err := tt.time.GobEncode()
+		if err == nil || err.Error() != tt.want {
+			t.Errorf("%v GobEncode error = %v, want %v", tt.time, err, tt.want)
+		}
+	}
+}

コアとなるコードの解説

time.go

  • gobError型とtimeGobVersion定数:
    • gobErrorは、gobエンコード/デコード中に発生する特定のエラーメッセージをラップするためのカスタムエラー型です。これにより、エラーの識別と処理が容易になります。
    • timeGobVersionは、time.Timegobエンコーディング形式のバージョンを示すバイト定数です。これにより、将来の形式変更に対する互換性が確保されます。
  • Time.GobEncode()メソッド:
    • このメソッドはtime.Time型の値をバイナリ形式に変換します。
    • まず、タイムゾーンのオフセットを分単位で計算し、int16型に収まるように検証します。UTCの場合は-1という特別な値を使用します。
    • 次に、timeGobVersiont.sec(秒)、t.nsec(ナノ秒)、offsetMin(分単位のオフセット)をそれぞれバイトに分解し、特定の順序でバイトスライスに格納します。t.secは8バイト、t.nsecは4バイト、offsetMinは2バイトで表現され、それぞれビットシフト演算子を使ってバイトに分割されます。
    • このバイトスライスがgobエンコーダに渡され、最終的なバイナリデータとして出力されます。
  • Time.GobDecode()メソッド:
    • このメソッドはバイナリデータからtime.Time型の値を復元します。
    • 入力されたバイトスライスの長さとバージョンバイトを検証し、不正なデータであればエラーを返します。
    • バイトスライスからt.sect.nsec、およびoffset(分単位)を読み取り、元の値に再構築します。ここでもビットシフト演算子を使ってバイトを結合し、元の整数値を復元します。
    • 最後に、デコードされたオフセットに基づいてtime.Locationを再構築します。-1*60(つまり-1分)であればUTC、ローカルタイムゾーンのオフセットと一致すればLocal、それ以外の場合はFixedZoneとしてロケーションを設定します。これにより、元のタイムゾーン情報が正確に復元されます。

time_test.go

  • bytesencoding/gobのインポート: gobエンコード/デコードのテストに必要なパッケージがインポートされています。
  • gobTests変数: GobEncodeGobDecodeのテストに使用されるtime.Time値の配列です。UTC、固定オフセット、ゼロ値、特定のUnix秒とナノ秒を持つ時間など、様々なケースを網羅しています。
  • TestTimeGob関数:
    • bytes.Bufferを介してgob.Encodergob.Decoderを作成します。
    • gobTestsの各Time値について、エンコードとデコードを実行します。
    • デコードされたTime値が元のTime値と等しいか(Equalメソッドを使用)、およびタイムゾーンの名前とオフセットが一致するかを検証します。これにより、時刻だけでなくロケーション情報も正しくシリアライズ・デシリアライズされていることを確認します。
  • invalidEncodingTests変数とTestInvalidTimeGob関数:
    • GobDecodeが不正な入力(空のバイトスライス、不正なバージョン、不正な長さ)を受け取った場合に、期待されるエラーメッセージを返すことを検証します。
  • notEncodableTimes変数とTestNotGobEncodableTime関数:
    • GobEncodeがエンコードできないはずのTime値(例: 分数オフセットを持つタイムゾーン、int16の範囲外のオフセット)を受け取った場合に、期待されるエラーメッセージを返すことを検証します。

これらの変更により、time.Time型はGoのgobパッケージと完全に互換性を持つようになり、Goアプリケーションでの日付と時刻のデータのシリアライズとデシリアライズが容易かつ堅牢に行えるようになりました。

関連リンク

参考にした情報源リンク