Time型をJSONに変換するとtime.Locationの情報が欠落する場合の対処法
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 // Hoge構造体を生成する h1 := Hoge{ Name: "aaa", CreatedAt: time.Unix(1593077427, 0), } // Jsonsに変換 j, _ := json.Marshal(h1) // Jsonから復元する 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" ) // Hoge構造体の定義は省略 func main() { time.Local = time.UTC // Hoge構造体を生成する h1 := Hoge{ Name: "aaa", CreatedAt: time.Unix(1593077427, 0), } // Jsonsに変換 j, _ := json.Marshal(h1) // Jsonから復元する 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 メソッドを使って判定する
こちらは time パッケージの Godoc に記載されている方法です。
result := h1.CreatedAt.Equal(h2.CreatedAt)
fmt.Printf("Time.Eqaul: %v", result)
出力結果は true になります。
Time.Eqaul: true
ただこの方法だと Time 型のフィールドを含む構造体同士を比較する場合はフィールドを一つずつ比較しないといけなくなります。
UTC に変換する
Time オブジェクトを生成するタイミングで UTC メソッドを使って UTC 変換に変換します。
func main() { time.Local = time.UTC // Hoge構造体を生成する h1 := Hoge{ Name: "aaa", // UTCに変換する CreatedAt: time.Unix(1593077427, 0).UTC(), } // Jsonsに変換 j, _ := json.Marshal(h1) // Jsonから復元する 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" ) // Hoge構造体の定義は省略 func main() { time.Local = time.UTC // Hoge構造体を生成する h1 := Hoge{ Name: "aaa", CreatedAt: time.Unix(1593077427, 0), } // Jsonsに変換 j, _ := json.Marshal(h1) // Jsonから復元する 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" ) // Hoge構造体の定義は省略 func main() { // nilをセットする time.Local = nil // Hoge構造体を生成する h1 := Hoge{ Name: "aaa", CreatedAt: time.Unix(1593077427, 0), } // Jsonsに変換 j, _ := json.Marshal(h1) // Jsonから復元する h2 := Hoge{} json.Unmarshal(j, &h2) result := reflect.DeepEqual(h1, h2) fmt.Printf("reflect.DeepEqual: %v\n", result) }
出力結果は以下のようになります。意図通り reflect.DeepEqual の結果が true になります。
reflect.DeepEqual: true