Goで構造体をcomparableとして使う場合の注意点
Go1.18から導入された型パラメータ comparable は、 == や != で比較可能な型のみを受け入れます。
mapのキーにジェネリックな型を指定するような関数でよく使うと思います。
func ToMap[T comparable](ss []T) map[T]struct{} {
m := make(map[T]struct{}, len(ss))
for _, s := range ss {
m[s] = struct{}{}
}
return m
}int や string などの組み込み型はもちろん、構造体も comparable を満たします。
type User struct {
ID int
Name string
}
func main() {
us := []User{
{
ID: 1,
Name: "1",
},
{
ID: 2,
Name: "2",
},
}
m := ToMap(us)
_, ok := m[User{ID: 1, Name: "1"}]
fmt.Println(ok) // true
}
ただし、構造体を使う場合、実体とポインタの判定の違いを理解しておかないとバグを生む可能性があります。
func main() {
{
us := []User{
{
ID: 1,
Name: "1",
},
{
ID: 2,
Name: "2",
},
}
m := ToMap(us)
_, ok := m[User{ID: 1, Name: "1"}]
fmt.Println(ok) // true
fmt.Println(us[0] == User{ID: 1, Name: "1"}) // true
}
{
us := []*User{
{
ID: 1,
Name: "1",
},
{
ID: 2,
Name: "2",
},
}
m := ToMap(us)
_, ok := m[&User{ID: 1, Name: "1"}]
fmt.Println(ok) // false
fmt.Println(us[0] == &User{ID: 1, Name: "1"}) // false
_, ok = m[us[0]]
fmt.Println(ok) // true
}
}
構造体の実体はフィールドが全て一致しているかどうかで判定され、ポインタはアドレスが一致しているかどうかで判定されます。
そのため、以下のように、スライスの要素から重複を削除して返す関数を作った場合、ポインタを要素に持つスライスを渡すと期待通りの結果にならなくなります。
func ToUnique[T comparable](ss []T) []T {
m := make(map[T]struct{}, len(ss))
unique := make([]T, 0, len(ss))
for _, s := range ss {
if _, ok := m[s]; !ok {
unique = append(unique, s)
m[s] = struct{}{}
}
}
return unique
}
func main() {
{
us := []User{
{
ID: 1,
Name: "1",
},
{
ID: 2,
Name: "2",
},
{
ID: 1,
Name: "1",
},
{
ID: 2,
Name: "2",
},
}
fmt.Println(ToUnique(us)) // [{1 1} {2 2}]
}
{
us := []*User{
{
ID: 1,
Name: "1",
},
{
ID: 2,
Name: "2",
},
{
ID: 1,
Name: "1",
},
{
ID: 2,
Name: "2",
},
}
fmt.Println(ToUnique(us)) // [0xc000010078 0xc000010090 0xc0000100a8 0xc0000100c0]
}
}
構造体を == で比較したりmapのキーにしたりする機会が殆どないので忘れてました。