TL;DR
キー値が同じ値だと思っていたら実は違っていたというオチです。
- キー項目として、
time.Time
型を含んだ構造体を使用した -
time.Time
型は、==
で比較してはいけない- 構造体を比較する場合、構造体の中にポインタ型が含まれている場合は、ポインタ値を比較する
-
time.Time
型は、*time.Location
型の項目を含んでいる
- マップのキーは、
==
がtrue
の場合、同値として判定する
結論は上記の通りですが、備忘録として、発生した経緯と調査の過程を下記に残したいと思います。
発覚するまで
例えば、データベースに以下のようなテーブルがあったとします。
ID | 名前 | 登録日時 |
---|---|---|
1 | 七尾 | 2017-06-29 12:00:00 |
2 | 高坂 | 2017-06-29 13:00:00 |
3 | 北沢 | 2017-06-29 14:00:00 |
4 | 矢吹 | 2017-06-29 15:00:00 |
このテーブルから、指定された回数の分だけランダムでレコードを取得し、レコードごとの抽出回数を集計する処理を実装しようとしていました。
最終的に以下のように出力する想定でした。
ID | 名前 | 抽出回数 |
---|---|---|
1 | 七尾 | 27 |
2 | 高坂 | 26 |
3 | 北沢 | 25 |
4 | 矢吹 | 22 |
その場合、以下のような処理を書けば目的は達成されるはずです。
const count = 100
type User struct {
ID int
Name string
RegisteredAt time.Time
}
func main() {
m := make(map[User]int)
for i := 0; i < count; i++ {
u := getUserRandom()
m[u]++
}
for u, count := range m {
fmt.Printf("%v %v %v\n", u.ID, u.Name, count)
}
}
func getUserRandom() model.User {
// データベースからランダムに1レコード取得する処理
}
上記の処理ですが、count
を増やすとおかしくなることがあります。以下のような結果が出てきたりします。
ID | 名前 | 抽出回数 |
---|---|---|
1 | 七尾 | 1500 |
1 | 七尾 | 1200 |
2 | 高坂 | 1200 |
2 | 高坂 | 1400 |
3 | 北沢 | 1500 |
3 | 北沢 | 1000 |
4 | 矢吹 | 1100 |
4 | 矢吹 | 1100 |
同一IDのデータが2行出力されていますね。明らかにおかしいです。
調査
マップ上で別項目として扱われている以上、必ず何かしらの値が違うはずです。とりあえずデバッグ出力してみます。
...
for u, count := range m {
// fmt.Printf("%v %v %v\n", u.ID, u.Name, count)
fmt.Printf("%#v\n", u)
}
...
User{ID:1, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634302000, loc:(*time.Location)(0xc00033d020)}}
User{ID:1, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634302000, loc:(*time.Location)(0xc00033dda0)}}
User{ID:2, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634305600, loc:(*time.Location)(0xc00033d020)}}
User{ID:2, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634305600, loc:(*time.Location)(0xc00033dda0)}}
User{ID:3, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634309200, loc:(*time.Location)(0xc00033d020)}}
User{ID:3, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634309200, loc:(*time.Location)(0xc00033dda0)}}
User{ID:4, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634312800, loc:(*time.Location)(0xc00033d020)}}
User{ID:4, Name:"", RegisteredAt:time.Time{wall:0x0, ext:63634312800, loc:(*time.Location)(0xc00033dda0)}}
1行目と2行目を比較すると、RegisteredAtのtime.Time
の中にあるloc
のポインタ値が異なっています。
マップのキーとして同一かどうかは ==
演算子で比較した結果true
かどうか で判定していると考えて良いです。
一方で、構造体の中にポインタ項目があった場合、ポインタのアドレスが同一かどうかで比較されます(参照先は比較しません)
その観点で考えると、1行目のUser != 2行目のUser
であるため、異なるキー項目として扱われるのは正しい。という結論になります。
上記を踏まえた回避策としては以下の2つが考えられます
-
time.Time
のタイムゾーンをUTC
に変更する-
loc
はnull
になる -
1行目のUser == 2行目のUser
となり、同一キーとして扱われるようになる
-
- そもそも
User
をキーとして使わない-
User.ID
などのプリミティブな値を使用する
-
今回についてはタイムゾーンを設定する必要があったため、後者を選択し、無事解決しました。
余談
time.Time
型を==
で比較してはいけないこと自体は知っていました。
if文などで条件分を記載する際であれば、仮に==
をタイプした時点で気付いたとは思いますが、今回はマップを使用した関係で原因を特定するまで気付きませんでした…気をつけなければ。
データベースからデータを取得した場合、このような現象が発生した原因については詳細を調べていません。
データベースから取得してtime.Time
構造体を生成する際、ロケーションは使いまわしているが、一定間隔で再生成するのでしょうか?