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構造体を生成する際、ロケーションは使いまわしているが、一定間隔で再生成するのでしょうか?