[インデックス 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
パッケージのインターフェース(GobEncoder
とGobDecoder
)を実装することで、この問題を解決し、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
パッケージで適切に扱われるように、GobEncode
とGobDecode
の2つのメソッドがtime.Time
型に追加されています。
GobEncode
の実装 (time.Time.GobEncode
)
- バージョンバイトの導入: エンコードされたデータの最初のバイトとして
timeGobVersion
(値は1) が導入されています。これは、将来的にエンコーディング形式が変更された場合に、後方互換性を維持するためのバージョン管理メカニズムです。 - タイムゾーンオフセットの処理:
t.Location()
がUTC (&utcLoc
) の場合、offsetMin
は-1
に設定されます。これは、UTCを特別な値として識別するためです。- それ以外の場合、
t.Zone()
から現在のタイムゾーンのオフセット(秒単位)を取得し、それを分単位に変換します。 - オフセットが分数の端数を持つ場合(例: 30秒のオフセット)や、
int16
の範囲外である場合、または-1
(UTCの識別子と衝突するため)である場合はエラーを返します。これは、gob
エンコーディングでタイムゾーンオフセットをint16
で表現するための制約です。
- データのエンコード:
timeGobVersion
(1バイト)t.sec
(int64): 8バイトでエンコードされます。これは、Unixエポックからの秒数を表します。t.nsec
(int32): 4バイトでエンコードされます。これは、秒未満のナノ秒を表します。offsetMin
(int16): 2バイトでエンコードされます。これは、UTCからの分単位のオフセットを表します。 これらの値は、ビッグエンディアン形式でバイトスライスに格納されます。
GobDecode
の実装 (*time.Time.GobDecode
)
- 入力検証:
- 入力バイトスライス
buf
が空の場合、エラーを返します。 - 最初のバイト(バージョンバイト)が
timeGobVersion
と一致しない場合、サポートされていないバージョンとしてエラーを返します。 - バイトスライスの長さが期待される長さ(バージョン1バイト + 秒8バイト + ナノ秒4バイト + ゾーンオフセット2バイト = 15バイト)と異なる場合、無効な長さとしてエラーを返します。
- 入力バイトスライス
- データのデコード:
buf
からt.sec
、t.nsec
、およびoffset
(分単位)をデコードします。エンコード時と同様に、ビッグエンディアン形式でバイトスライスから値を読み取ります。
- タイムゾーンの復元:
- デコードされた
offset
が-1 * 60
(つまり、offsetMin
が-1
だった場合)であれば、ロケーションをUTC (&utcLoc
) に設定します。 - それ以外の場合、デコードされたオフセットが現在のシステムのローカルタイムゾーンのオフセットと一致するかどうかをチェックします。一致すれば、ロケーションを
Local
に設定します。 - 上記いずれにも該当しない場合、デコードされたオフセットに基づいて
FixedZone
を作成し、ロケーションを設定します。これにより、元のタイムゾーン情報が正確に復元されます。
- デコードされた
エラーハンドリング
gobError
というカスタムエラー型が定義されており、エンコード/デコード中に発生する特定のエラー(例: 分数オフセット、範囲外のオフセット、無効なデータ形式)を報告するために使用されます。
テストの追加 (src/pkg/time/time_test.go
)
新しいGobEncode
とGobDecode
メソッドの正確性を検証するために、包括的なテストケースが追加されています。
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.Time
のgob
エンコーディング形式のバージョンを示すバイト定数です。これにより、将来の形式変更に対する互換性が確保されます。
Time.GobEncode()
メソッド:- このメソッドは
time.Time
型の値をバイナリ形式に変換します。 - まず、タイムゾーンのオフセットを分単位で計算し、
int16
型に収まるように検証します。UTCの場合は-1
という特別な値を使用します。 - 次に、
timeGobVersion
、t.sec
(秒)、t.nsec
(ナノ秒)、offsetMin
(分単位のオフセット)をそれぞれバイトに分解し、特定の順序でバイトスライスに格納します。t.sec
は8バイト、t.nsec
は4バイト、offsetMin
は2バイトで表現され、それぞれビットシフト演算子を使ってバイトに分割されます。 - このバイトスライスが
gob
エンコーダに渡され、最終的なバイナリデータとして出力されます。
- このメソッドは
Time.GobDecode()
メソッド:- このメソッドはバイナリデータから
time.Time
型の値を復元します。 - 入力されたバイトスライスの長さとバージョンバイトを検証し、不正なデータであればエラーを返します。
- バイトスライスから
t.sec
、t.nsec
、およびoffset
(分単位)を読み取り、元の値に再構築します。ここでもビットシフト演算子を使ってバイトを結合し、元の整数値を復元します。 - 最後に、デコードされたオフセットに基づいて
time.Location
を再構築します。-1*60
(つまり-1
分)であればUTC、ローカルタイムゾーンのオフセットと一致すればLocal
、それ以外の場合はFixedZone
としてロケーションを設定します。これにより、元のタイムゾーン情報が正確に復元されます。
- このメソッドはバイナリデータから
time_test.go
bytes
とencoding/gob
のインポート:gob
エンコード/デコードのテストに必要なパッケージがインポートされています。gobTests
変数:GobEncode
とGobDecode
のテストに使用されるtime.Time
値の配列です。UTC、固定オフセット、ゼロ値、特定のUnix秒とナノ秒を持つ時間など、様々なケースを網羅しています。TestTimeGob
関数:bytes.Buffer
を介してgob.Encoder
とgob.Decoder
を作成します。gobTests
の各Time
値について、エンコードとデコードを実行します。- デコードされた
Time
値が元のTime
値と等しいか(Equal
メソッドを使用)、およびタイムゾーンの名前とオフセットが一致するかを検証します。これにより、時刻だけでなくロケーション情報も正しくシリアライズ・デシリアライズされていることを確認します。
invalidEncodingTests
変数とTestInvalidTimeGob
関数:GobDecode
が不正な入力(空のバイトスライス、不正なバージョン、不正な長さ)を受け取った場合に、期待されるエラーメッセージを返すことを検証します。
notEncodableTimes
変数とTestNotGobEncodableTime
関数:GobEncode
がエンコードできないはずのTime
値(例: 分数オフセットを持つタイムゾーン、int16
の範囲外のオフセット)を受け取った場合に、期待されるエラーメッセージを返すことを検証します。
これらの変更により、time.Time
型はGoのgob
パッケージと完全に互換性を持つようになり、Goアプリケーションでの日付と時刻のデータのシリアライズとデシリアライズが容易かつ堅牢に行えるようになりました。
関連リンク
- Go Issue 2526:
time.Time
gob marshaler: https://github.com/golang/go/issues/2526 - Go CL 5448114:
time: gob marshaler for Time
: https://golang.org/cl/5448114
参考にした情報源リンク
- Go
encoding/gob
package documentation: https://pkg.go.dev/encoding/gob - Go
time
package documentation: https://pkg.go.dev/time - Go
time.Time
struct: https://pkg.go.dev/time#Time - Go
time.Location
struct: https://pkg.go.dev/time#Location - Go
GobEncoder
interface: https://pkg.go.dev/encoding/gob#GobEncoder - Go
GobDecoder
interface: https://pkg.go.dev/encoding/gob#GobDecoder