🛠️

Goで型に縛られない良い感じのContains関数の書き方

4 min read

はじめに

Go の標準パッケージに、スライスや配列に特定の値が含まれるかを判定する Contains 関数が存在しないため自作しました。
JavaScript の Array.includes メソッドとか Python の in 演算子みたいな機能です。イメージとしては以下のようなものです。

// int型のスライス
containsInt := Contains([]int{1, 2, 3, 4, 5}, 3)
fmt.Println(containsInt) // -> true
// string型のスライス
containsString := Contains([]string{"apple", "orange", "lemon"}, "orange")
fmt.Println(containsString) // -> true
// 構造体のスライス
type hero struct {
    Name string
}
heros := []hero{hero{"Luke"}, hero{"Han"}, hero{"Leia"}}
containsStruct := Contains(heros, hero{"Han"})
fmt.Println(containsStruct) // -> true

Containsの定番実装がない

Go の Contains 関数に関して Stack Overflow の Contains method for a slice で活発に議論されていますが、実装例の多くが特定の型向けに特化した実装です。

// int型専用のContains関数
func Contains(s []int, e int) bool {
    for _, a := range s {
        if a == e {
            return true
        }
    }
    return false
}

スライスの型に縛られずにどんな型(int, float64, string, 構造体)にも使える関数の実装例が意外とありませんでした。

Contains関数の実装

特定の型に縛られず int や float64 などプリミティブ型から構造体のスライスまでカバーする Contains 関数の実装は以下になります。

func Contains(list interface{}, elem interface{}) bool {
    listV := reflect.ValueOf(list)

    if listV.Kind() == reflect.Slice {
        for i := 0; i < listV.Len(); i++ {
            item := listV.Index(i).Interface()
	    // 型変換可能か確認する
	    if !reflect.TypeOf(elem).ConvertibleTo(reflect.TypeOf(item)) {
	        continue
            }
            // 型変換する
            target := reflect.ValueOf(elem)
	        .Convert(reflect.TypeOf(item)).Interface()
	    // 等価判定をする
            if ok := reflect.DeepEqual(item, target); ok {
                return true
            }
        }
    }
    return false
}

リフレクション使ってスライスを操作しつつ、スライスのメンバを reflect.Value.Convert 関数を使って検索対象の値の型に変換しているところがポイントです。
reflect.Value.Convert メソッドは型変換に失敗するとパニックが発生するので変換をする前に reflect.Type.ConvertibleTo メソッドを使って型変換可能かどうか確認しています。

当初は ConvertibleTo メソッドを使っておらず、パニックが発生してしまう実装にしてましたが @gorilla0513@tenntenn さんからご指導いただきパニックを発生させないように修正しました。この場を借りて御礼申し上げます。

Contains関数の使い方

使い方は以下になります。第一引数にスライス、第二引数に検索対象の値を指定します。int32やfloat32などGoのプリミティブ型のスライスから構造体のスライスまで対応しています。

// int32のスライス
containsInt32 := Contains([]int32{1, 2, 3, 4, 5}, 3)
fmt.Println(containsInt32) // -> true
// intのスライス
containsInt := Contains([]int{1, 2, 3, 4, 5}, 2)
fmt.Println(containsInt) // -> true
// float64のスライス
containsFloat64 := Contains([]float64{1.1, 2.2, 3.3, 4.4, 5.5}, 4.4)
fmt.Println(containsFloat64) // -> true
// string型のスライス
containsString := Contains([]string{"apple", "orange", "lemon"}, "orange")
fmt.Println(containsString) // -> true
// 構造体のスライス
type item struct {
    ID   string
    Name string
}
list := []item{
    item{
        ID:   "1",
        Name: "test1",
    },
    item{
        ID:   "2",
        Name: "test2",
    },
    item{
        ID:   "3",
        Name: "test3",
    },
}
target := item{
	ID:   "2",
	Name: "test2",
}
containsStruct := Contains(list, target)
fmt.Println(containsStruct) // -> true

まとめ

  • Contains関数は使う場面が多い割にこれといった定番実装がない
  • リフレクションを使うとプリミティブ型から構造体までカバーする Contains 関数を作ることができる
  • リフレクションを使っているので特定の型向けに特化した実装に比べると実行速度は遅くなると思われます(ちゃんと計測してませんが)

将来Goにジェネリクスが実装されればこんなことしなくてすむ(はず)なので、それまではこの関数でしのぎたいと思います。この記事で紹介した関数の実装はこちらに置いてあるのでユニットテスト含め参考にしてください。

参考

Discussion

ログインするとコメントできます