Time 型のフィールドを含む構造体を JSON にエンコード、JSON からデコードすると特定条件下で Time 型オブジェクトの time.Location の情報が失われることがあります。
以下のプログラムを見てください。
package main
import (
"fmt"
"time"
"encoding/json"
)
type Hoge struct {
Name string
CreatedAt time.Time
}
func main() {
time.Local = time.UTC
h1 := Hoge{
Name: "aaa",
CreatedAt: time.Unix(1593077427, 0),
}
j, _ := json.Marshal(h1)
h2 := Hoge{}
json.Unmarshal(j, &h2)
fmt.Printf("h1の出力結果\n%#v\n", h1)
fmt.Printf("h2の出力結果\n%#v\n", h2)
}
出力結果を確認すると、JSON 変換前の Hoge オブジェクト(変数名 h1)の CreatedAt フィールドは loc に (*time.Location)(0x5c7a40)
がセットされているのに対し、JSON 変換後に復元した Hoge オブジェクト(変数名 h2)の CreatedAt フィールドは loc が nil になっています。
h1の出力結果
main.Hoge{Name:"aaa", CreatedAt:time.Time{wall:0x0, ext:63728674227, loc:(*time.Location)(0x5c7a40)}}
h2の出力結果
main.Hoge{Name:"aaa", CreatedAt:time.Time{wall:0x0, ext:63728674227, loc:(*time.Location)(nil)}}
これ %+v
で出力しても違いがわからないので注意が必要です。%#v
で出力してみないとわからないです。
time.Localにtime.UTCをセットした場合のみ起きる現象
JSON に変換すると Time 型フィールドの loc 情報が nil になってしまう現象は time.Localにtime.UTCをセットした場合のみ発生します。
func main() {
time.Local = time.UTC
...
... その他、処理
...
}
以下のように UTC 以外のローカルタイムを設定している場合は発生しません。
func main() {
loc, _ := time.LoadLocation("Asia/Tokyo")
time.Local = loc
...
... その他、処理
...
}
ローカル時間をUTCにセットするとreflect.DeepEqualが常にfalse判定されてしまう
time.Location の情報が失われると、Time 型フィールドを含む構造体を reflect.DeepEqual で比較した時に意図通り判定されなくなります。以下のプログラムを見てください。
package main
import (
"fmt"
"time"
"encoding/json"
"reflect"
)
func main() {
time.Local = time.UTC
h1 := Hoge{
Name: "aaa",
CreatedAt: time.Unix(1593077427, 0),
}
j, _ := json.Marshal(h1)
h2 := Hoge{}
json.Unmarshal(j, &h2)
result := reflect.DeepEqual(h1, h2)
fmt.Printf("reflect.DeepEqual: %v", result)
}
result
の出力結果は true ではなく false になります。
reflect.DeepEqual: false
対処方法
これに対処する方法はいくつか存在します。
- Time.Equal メソッドを使って判定する
- UTC に変換する
- go-cmp を使って判定する
- time.Local に nil をセットする
個別に対処方法を見ていきましょう
Time.Equal メソッドを使って判定する
こちらは time パッケージの Godoc に記載されている方法です。
result := h1.CreatedAt.Equal(h2.CreatedAt)
fmt.Printf("Time.Eqaul: %v", result)
出力結果は true になります。
Time.Eqaul: true
ただこの方法だと Time 型のフィールドを含む構造体同士を比較する場合はフィールドを一つずつ比較しないといけなくなります。
Time オブジェクトを生成するタイミングで UTC メソッドを使って UTC 変換に変換します。
func main() {
time.Local = time.UTC
h1 := Hoge{
Name: "aaa",
CreatedAt: time.Unix(1593077427, 0).UTC(),
}
j, _ := json.Marshal(h1)
h2 := Hoge{}
json.Unmarshal(j, &h2)
result := reflect.DeepEqual(h1, h2)
fmt.Printf("reflect.DeepEqual: %v", result)
}
Hoge 構造体を生成するときに UTC メソッドを使って変換する必要がありますが結果はちゃんと true になります。
reflect.DeepEqual: true
Equal メソッドを使うよりも良さそうではありますが UTC メソッド呼び出しを忘れるとバグの温床になります。個人的にはあまりおすすめしません。
go-cmp を使って判定する
reflect.DeepEqual を使う代わりに、ユニットテストで構造体を比較する時の定番パッケージ go-cmp を使って比較します。
import (
"fmt"
"time"
"encoding/json"
"github.com/google/go-cmp/cmp"
"reflect"
)
func main() {
time.Local = time.UTC
h1 := Hoge{
Name: "aaa",
CreatedAt: time.Unix(1593077427, 0),
}
j, _ := json.Marshal(h1)
h2 := Hoge{}
json.Unmarshal(j, &h2)
result1 := reflect.DeepEqual(h1, h2)
fmt.Printf("reflect.DeepEqual: %v\n", result1)
result2 := cmp.Equal(h1, h2)
fmt.Printf("cmp.Equal: %v", result2)
}
出力結果は以下のようになります。
reflect.DeepEqual: false
cmp.Equal: true
go-cmp は Time 型フィールドを比較する時、内部で Time.Equal メソッドを使って判定してくれるので reflect.DeepEqual が false 判定してしまう場面でも意図通り判定してくれます。
time.Local に nil をセットする
go-cmp も良いのですが Go の標準パッケージだけでなんとかするなら、main 関数で time.Local に nil をセットする方法があります。
package main
import (
"fmt"
"time"
"encoding/json"
"reflect"
)
func main() {
time.Local = nil
h1 := Hoge{
Name: "aaa",
CreatedAt: time.Unix(1593077427, 0),
}
j, _ := json.Marshal(h1)
h2 := Hoge{}
json.Unmarshal(j, &h2)
result := reflect.DeepEqual(h1, h2)
fmt.Printf("reflect.DeepEqual: %v\n", result)
}
出力結果は以下のようになります。意図通り reflect.DeepEqual の結果が true になります。
reflect.DeepEqual: true
参考記事