背景
Goではマルチタイプの自作汎用メソッドを実装したい時に、ジェネリクスが使えない代わりにinterface型やreflect
パッケージを用いたり、個別型ごとにメソッドを定義したりして対応する方法がよく使われます。
単に基本型をinterface型として扱うだけであれば、実装はさほど難しくないと思われますが、配列やスライスが含まれる場合にはちょっと工夫が必要になるかと思います。
そこで本記事ではinteface{}やreflect
パッケージを用いて、型ごとのswitch分岐を記述することなく、配列を含む様々な基本型を引数や返り値に扱う汎用メソッドを実装する方法について紹介します。また、reflect
パッケージの利用が最小限に抑えられそうな汎用メソッドのサンプルとしてChoice
,Sample
,Intersection
メソッドの作り方を後半に載せてあります。
for _, v := range values {
if v == nil {
fmt.Println("it is a nil")
} else {
switch v.(type) {
case int:
fmt.Println("it is a int")
case string:
fmt.Println("it is a string")
case int64:
fmt.Println("it is a int64")
// other type ....
default:
fmt.Println("i don't know it")
}
}
}
課題
[]interface型を[]intや[]stringに型キャストや型アサーションすることができない
Go経験者であれば一度はやろうとしたことはあるかもしれないですが、Goでは直接この変換をしようとするとコンパイルエラーとなります。また、そもそも型アサーション(v.(int)
やv.(string)
等)はinterface型が使えるものであって、[]interface型は使えないので注意が必要です。
実際に変換したい場合、以下のようにループにより要素型であるinterface型ごとに型アサーションをすれば変換可能です。
func main() {
from := make([]interface{}, 10)
to := make([]int, 10)
for i := 0; i < 10; i++ {
from[i] = i
}
for i, v := range from {
to[i] = v.(int)
}
fmt.Println(to)
}
しかし、このような変換処理を毎回実施することは極力避けたいため、以下のように呼び出し側の型アサーションを行うだけで利用可能な、配列を含む様々な基本型に対応した汎用メソッドの実装を目指します。
具体的なサンプルメソッドについては後述します。
func main(){
type User struct {
email string
age int
}
val1 := targetMethod1([]int{1,2,3,4,6}).([]int)
val2 := targetMethod1([]int{1,2,3,4,6}).([]string)
val3 := targetMethod1([]string{"apple","banana","peach"}).([]int64)
val4 := targetMethod1([]string{"北海道","東京都","福岡県"}).([]bool)
// 型アサーションの結果(bool)は返り値の二つ目の要素としても取得できる
val5, ok := targetMethod2([]int{1,4,3},3).(int)
val6, ok := targetMethod2([]string{"apple","banana","peach"},2).(string)
val7, ok := targetMethod2([]User{
{email:"test1@gmail.com", age:25},
{email:"test2@gmail.com", age:23}
},30).(bool)
}
// 説明のためT,Sを総称型として表現しています。
// 引数と返り値はそれぞれ[S]型、[T]型の想定
func targetMethod1(list interface{}) interface{}{
result := reflect.ValueOf(list)
// 何かの処理
// ....
// ....
return result.Interface()
}
// 引数は[S]型,int型の想定、返り値はT型の想定
func targetMethod2(list interface{},n int) interface{}{
result := reflect.ValueOf(list)
// 何かの処理
// ....
// ....
return result.Interface()
}
汎用メソッドの実装方針
ポイントは以下の3点になります。
- 配列や構造体等を含む総称型としてinterface型を利用する
reflect.ValueOf
を利用して、interface型のチェックを行う- []interface型の代わりに
reflect.MakeSlice
を利用する
1.については前述の通り、基本型の配列を直接[]interfaceとして扱えないため、配列を含む場合に関わらず、interface型を利用します。こちらは汎用メソッドを作る場合の最もベーシックな手段になるかと思います。
2.については、まず、interface型を引数として受け取る場合、基本型や基本型の配列などが分け隔てなく受け取れてしまう問題があります。配列の場合、後続の処理でループ処理が考えられるため、処理を切り分ける必要があります。そこでreflect.ValueOf
を使いreflect.Value
型として取り込むことで、interface型の内部の情報が取得できるようになり、型の種別に応じた処理の切り分けが可能になります。
3.の[]interface型を使用しない理由は、前述の[]interface型から基本型の配列への直接変換ができない以上、型別のループによる詰み直しが必要になるためです。今回は呼び出し側で基本型や基本型の配列として扱える汎用メソッドの利用を想定しているため、本記事では引数や返り値に[]interface型を使わない方針としています。その代わりに抽象型配列と利用できるreflect.MakeSlice
を使います。こちらの説明や[]interface型を利用した場合の問題についてはサンプルで紹介します。
汎用メソッド内の具体的な制御についてはreflectパッケージの他のメソッドを利用することになるかと思いますが、上記はおおよそ共通して抑えるべきポイントかと思います。
GOバージョン
$ go version
go version go1.17.2 darwin/amd64
汎用メソッドの実装例
上記の方針を踏まえて、具体的な汎用メソッドのいくつか実装してみます。
-
Choice
:配列からランダムに要素を一つ抽出するメソッドの実装(Pythonで言うところのrandom.choice()に相当) -
Sample
:配列からランダムに要素を複数抽出(重複なし)するメソッドの実装(Pythonで言うところのrandom.sample()に相当) -
Intersection
:2つの配列の重複なしの共通項の要素配列(順不同)を取得するメソッドの実装
※汎用メソッドでは様々な型が受け取れてしまう反面、状況によってはエラー制御が多岐に渡り、その返し方はケースバイケースのため、本記事では引数のチェックは必要最低限のものに抑え、不適切な引数についてはnilを返す対応で統一しています。
func Choice(list interface{}) interface{}
引数
list
: 対象の配列
返り値
list
内の一つの要素
利用例
Choice([banana apple peach]) = peach
Choice([0,1,2,3,4,5,6]) = 5
実装例
func Choice(list interface{}) interface{}{
listV := reflect.ValueOf(list)
if listV.Kind() != reflect.Slice || listV.Len() == 0 {
return nil
}
randIndex := rand.Intn(listV.Len())
return listV.Index(randIndex).Interface()
}
まず、ポイントとして引数のinterface型は配列として扱う必要があるのに対し、返り値は配列でない基本型として扱う必要があります。reflect
パッケージでは型の種別にKind型を持ち、reflect.Slice
と一致するかどうかで受け取ったinterface
型がスライスであるかどうかを判定することができます。また、Index
メソッドにより配列の要素にアクセスできるので、その結果をInterface
メソッドによりinterface
型に変換することで、返り値として配列の要素を設定することができます。
また、この実装ではrand
パッケージを利用しているので、呼び出し側でrand.Seed(time.Now().UnixNano())
のような初期化が必要になります。
func Sample(list interface{}, n int) interface{}
ある配列からN個の要素をサンプリングする方法についてはハッシュテーブルを利用しないスマートなやり方が以下の公式issueに上がっていたので、こちらをベースに実装しています。
https://github.com/golang/go/issues/23717
引数
list
: 対象の配列
n
: 選択する個数
返り値
list
のn個の要素をランダムに抽出した配列(順不動)
利用例
Sample([赤玉 黒玉 白玉 黄玉 青玉],2) = [白玉 赤玉]
Sample([1 2 3 4 5 6 7],3) = [5 2 3]
実装例
func Sample(list interface{}, n int) interface{}{
listV := reflect.ValueOf(list)
if listV.Kind() != reflect.Slice || listV.Len() == 0 || listV.Len() < n{
return nil
}
sample := reflect.MakeSlice(listV.Type(),listV.Len(),listV.Len())
reflect.Copy(sample,listV) // 引数の配列の順番を書き換えないようにコピーする
swap := reflect.Swapper(sample.Interface())
rand.Shuffle(listV.Len(), func(i,j int) {
swap(i,j)
})
return sample.Slice(0,n).Interface()
}
やっていることは、元々の配列を適当に並び替えてから先頭のN個を取ってくれば、重複なしにランダムに取ってきたのと同じ結果になるというものです。
ポイントはreflect.Copy
しているところで、reflect.Value
型は参照を取り扱っているため、うっかりreflect.Swapper
に元配列を渡してしまうと、元々の配列もランダムな順番に書き換えてしまうことになります。そのため、作業用の配列に一度値コピーする必要があります。このような作業配列にはreflect.MakeSlice
が使えます。こちらはreflect.Value
型なのですが、reflect
パッケージ内で配列型のように扱うことができます。
reflect.MakeSlice
は三つの引数として(配列の型、要素数、最大容量)
を設定できますが、多くケースでは次のどちらかを設定することで作業配列が使えるようになります。
初期化する場合: reflect.MakeSlice(target.Type(),0,0)
コピー配列を用意する場合: reflect.MakeSlice(target.Type(),target.Len(),target.Len())
また、この例で言及すべき点として、以下のようにreflect
パッケージを使わず、[]interface
型を用いて以下のようにも実装できそうに見えます。
func Sample(list interface{}, n int) interface{}{
sample := make([]interface{},len(list.([]interface{})))
rand.Shuffle(len(sample),func(i,j int){
sample[i],sample[j] = sample[j],sample[i]
})
return sample[:n]
}
reflect使わない方がシンプルじゃんっと思ってしまいそうですが、実は落とし穴があります。それはlist.([]interface{})
と型アサーションしている所です。コンパイル自体は問題なく通るのですが、実行時に[]int{1,2,3,4,5}
のような基本型の配列を入れてみようものなら、以下のように怒られてしまいます。
panic: interface conversion: interface {} is []int, not []interface {}
このようなことからも、汎用メソッド内で型を認識することなく抽象リストを扱いたい場合は、reflect
パッケージを利用するのが良さそうに思います。
func Intersection(list1 interface{},list2 interface{}) interface{}{
引数
list1
: 配列A
list2
: 配列B
※配列Aと配列Bは同じ型である必要がある
返り値
list1
とlist2
の共通要素で構成されたユニークな配列(順不同)
利用例
Intersection([1,2,3,4,5,6],[0,2,4,6,8]) = [2,4,6]
Intersection([python,rust,go,c#],[c++,go,java,rust]) = [rust,go]
実装例
func Intersection(list1 interface{},list2 interface{}) interface{}{
list1V := reflect.ValueOf(list1)
if list1V.Kind() != reflect.Slice{
return nil
}
list2V := reflect.ValueOf(list2)
if list2V.Kind() != reflect.Slice{
return nil
}
if list1V.Type() != list2V.Type(){
return nil
}
set := make(map[interface{}]struct{})
for i:=0;i<list1V.Len();i++{
set[list1V.Index(i).Interface()] = struct{}{}
}
intersection := reflect.MakeSlice(list1V.Type(),0,0)
unique := make(map[interface{}]struct{})
for i:=0;i<list2V.Len();i++{
elem := list2V.Index(i).Interface()
if _,has:=unique[elem];has {
// 重複要素は含めない
continue;
}
if _,has := set[elem];has{
intersection = reflect.Append(intersection,list2V.Index(i))
}
unique[elem] = struct{}{}
}
return intersection.Interface()
}
前述の二つの例を通して、おおよそ理解可能な内容かと思いますが、些細な点として、reflect
パッケージにはreflect.MakeMap
というmap型の抽象型が存在するのですが、キーの値をinterface型にすることで、こちらの利用なく上記のように通常のmap型を利用したset型表現で実装も可能であることです。もちろんスライスについては[]interface型を避けるために、reflect.MakeSlice
を利用しています。
汎用メソッドのテスト
type Pair struct {
p int
q int
}
func TestChoice(t *testing.T) {
// Containsで利用する乱数の初期化
rand.Seed(time.Now().UnixNano())
{
input := []int{1, 2, 3, 4, 5, 6}
actual, ok := utils.Choice(input).(int)
assert.True(t, ok) // 型アサーションのチェック
assert.True(t, Contains(input, actual)) // 取得する値はランダムなため、含有チェックで確認します。
}
{
input := []string{"a", "b", "c", "d", "e"}
actual := utils.Choice(input).(string)
assert.True(t, Contains(input, actual))
}
{
input := []int64{3}
actual, ok := utils.Choice(input).(int64)
assert.True(t, ok)
assert.True(t, Contains(input, actual))
}
{
inputList := []Pair{{p: 3, q: 4}, {p: 10, q: 5}, {p: 20, q: 40}}
actualInt, ok := utils.Choice(inputList).(Pair)
assert.True(t, ok)
assert.True(t, Contains(inputList, actualInt))
}
{
// 配列が空のとき
input := []struct{}{}
actual, ng := utils.Choice(input).(struct{})
assert.False(t, ng)
assert.Equal(t, struct{}{}, actual)
}
{
// 型アサーションの指定が間違っている時
input := []int{1, 2, 3, 4, 5}
actual, ng := utils.Choice(input).(string)
assert.False(t, ng)
assert.Equal(t, "", actual) // 結果を無視して誤った型のデフォルト値と一致する
}
}
func TestSample(t *testing.T) {
{
n := 3
input := []int{1, 2, 3, 4, 5, 6}
actual, ok := utils.Sample(input, n).([]int)
assert.True(t, ok) // 型アサーションのチェック
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, input) // 中身が書き換わっていないかチェック
assert.Equal(t, n, len(actual)) // 取得している個数がnと一致しているかチェック
assert.Equal(t, actual, Unique(actual).([]int)) // 重複が含まれていないかどうかのチェック
for _, v := range actual {
assert.True(t, Contains(input, v)) // 取得する値はランダムなため、含有チェックで確認します。
}
}
{
n := 1
input := []string{"a", "b", "c", "d", "e"}
actual, ok := utils.Sample(input, n).([]string)
assert.True(t, ok)
assert.Equal(t, []string{"a", "b", "c", "d", "e"}, input)
assert.Equal(t, n, len(actual))
assert.Equal(t, actual, Unique(actual).([]string))
for _, v := range actual {
assert.True(t, Contains(input, v))
}
}
{
n := 1
input := []int64{3}
actual, ok := utils.Sample(input, n).([]int64)
assert.True(t, ok)
assert.Equal(t, []int64{3}, actual)
assert.Equal(t, actual, Unique(actual).([]int64))
for _, v := range actual {
assert.True(t, Contains(input, v))
}
}
{
n := 3
input := []Pair{{p: 3, q: 4}, {p: 10, q: 5}, {p: 20, q: 40}}
actual, ok := utils.Sample(input, n).([]Pair)
assert.True(t, ok)
assert.Equal(t, []Pair{{p: 3, q: 4}, {p: 10, q: 5}, {p: 20, q: 40}}, input)
assert.Equal(t, n, len(actual))
assert.Equal(t, actual, Unique(actual).([]Pair))
for _, v := range actual {
assert.True(t, Contains(input, v))
}
}
{
// 配列が空のとき
n := 4
input := []struct{}{}
actual, ng := utils.Sample(input, n).([]struct{})
assert.Equal(t, []struct{}{}, input)
assert.False(t, ng)
assert.Nil(t, actual)
}
{
// 型アサーションの指定が間違っている時
n := 3
input := []int{1, 2, 3, 4, 5}
actual, ng := utils.Sample(input, n).(string)
assert.Equal(t, []int{1, 2, 3, 4, 5}, input)
assert.False(t, ng)
assert.Equal(t, "", actual) // 結果を無視して誤った型のデフォルト値と一致する
}
{
// nの値が配列サイズを超えている時
n := 6
input := []int{1, 2, 3, 4, 5}
actual, ng := utils.Sample(input, n).([]int)
assert.Equal(t, []int{1, 2, 3, 4, 5}, input)
assert.False(t, ng)
assert.Nil(t, actual)
}
}
func TestIntersection(t *testing.T) {
{
listA := []int{1, 2, 3, 4, 5, 6, 7, 8}
listB := []int{2, 4, 6, 8, 10, 12}
expect := []int{2, 4, 6, 8}
actual, ok := utils.Intersection(listA, listB).([]int)
assert.True(t, ok) // 型アサーションチェック
assert.Equal(t, len(expect), len(actual)) // 配列のサイズチェック
sort.Sort(sort.IntSlice(expect))
sort.Sort(sort.IntSlice(actual))
assert.Equal(t, expect, actual) // 順番を揃えて配列をチェック
}
{
listA := []int{1, 2, 2, 3, 3, 3}
listB := []int{1, 2, 2, 3, 3, 3}
expect := []int{1, 2, 3}
actual, ok := utils.Intersection(listA, listB).([]int)
assert.True(t, ok)
assert.Equal(t, len(expect), len(actual))
assert.Equal(t, expect, actual)
}
{
listA := []string{"python", "rust", "golang", "javascript", "c#"}
listB := []string{"c++", "golang", "java", "rust"}
expect := []string{"golang", "rust"}
actual, ok := utils.Intersection(listA, listB).([]string)
assert.True(t, ok)
assert.Equal(t, len(expect), len(actual))
sort.Sort(sort.StringSlice(expect))
sort.Sort(sort.StringSlice(actual))
assert.Equal(t, expect, actual)
}
{
listA := []Pair{{p: 3, q: 4}, {p: 10, q: 5}, {p: 20, q: 40}}
listB := []Pair{{p: 4, q: 3}, {p: 10, q: 6}, {p: 20, q: 40}, {p: 20, q: 40}, {p: 3, q: 3}}
expect := []Pair{{p: 20, q: 40}}
actual, ok := utils.Intersection(listA, listB).([]Pair)
assert.True(t, ok)
assert.Equal(t, len(expect), len(actual))
assert.Equal(t, expect, actual)
}
{
listA := []int{1, 2, 3, 4, 4, 4}
listB := []int{5, 6, 7, 8}
expect := []int{}
actual, ok := utils.Intersection(listA, listB).([]int)
assert.True(t, ok)
assert.Equal(t, len(expect), len(actual))
assert.Equal(t, expect, actual)
}
{
listA := []int{1, 2, 3, 4, 4, 4}
listB := []int{5, 6, 7, 8}
expect := []int{}
actual, ok := utils.Intersection(listA, listB).([]int)
assert.True(t, ok)
assert.Equal(t, len(expect), len(actual))
assert.Equal(t, expect, actual)
}
{
listA := []struct{}{}
listB := []struct{}{}
expect := []struct{}{}
actual, ok := utils.Intersection(listA, listB).([]struct{})
assert.True(t, ok)
assert.Equal(t, len(expect), len(actual))
assert.Equal(t, expect, actual)
}
{
listA := []int{1, 2, 3}
listB := []string{"1", "2", "3"}
actual, ng := utils.Intersection(listA, listB).([]int)
assert.Nil(t, actual)
assert.False(t, ng)
}
{
listA := []int{1, 2, 3}
listB := []int{0, 2}
actual, ng := utils.Intersection(listA, listB).([]string)
assert.Nil(t, actual)
assert.False(t, ng)
}
}
// Contains listにelemが含まれていればtrue,含まれていなければfalseを返す
func Contains(list interface{}, elem interface{}) bool {
listV := reflect.ValueOf(list)
if listV.Kind() != reflect.Slice {
return false
}
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
}
// Unique listの要素の重複を排除する
func Unique(list interface{}) interface{} {
listV := reflect.ValueOf(list)
if listV.Kind() != reflect.Slice {
return nil
}
unique := reflect.MakeSlice(listV.Type(), 0, 0)
set := make(map[interface{}]struct{})
for i := 0; i < listV.Len(); i++ {
if _, has := set[listV.Index(i).Interface()]; has {
continue
}
unique = reflect.Append(unique, listV.Index(i))
set[listV.Index(i).Interface()] = struct{}{}
}
return unique.Interface()
}
$ go test -v util_test.go
=== RUN TestChoice
--- PASS: TestChoice (0.00s)
=== RUN TestSample
--- PASS: TestSample (0.00s)
=== RUN TestIntersection
--- PASS: TestIntersection (0.00s)
PASS
ok command-line-arguments 0.256s
問題なさそうです。
おまけ
サンプルで紹介したChoice
メソッドでrand
パッケージを使用しない実装例についても載せておきます。こちらはSample
の取得個数を1とした場合の考えを利用したものになります。※乱数設定が不要になる反面、配列コピーを要するのでサイズが大きい時は乱数版の方が良さそう。
func Choice(values interface{}) interface{}{
listV := reflect.ValueOf(values)
if listV.Kind() != reflect.Slice || listV.Len() == 0 {
return nil
}
sample := reflect.MakeSlice(listV.Type(),listV.Len(),listV.Len())
reflect.Copy(sample,listV)
swap := reflect.Swapper(sample.Interface())
rand.Shuffle(listV.Len(), func(i,j int) {
swap(i,j)
})
return sample.Index(0).Interface()
}
終わりに
reflectパッケージを使った様々な基本型に対応したインタフェースを持つ汎用メソッドの実装方法について紹介しました。今回reflectパッケージで使ったメソッドはごく一部の簡単なものになりますが、引数に複雑な構造体を想定する場合などは他のメソッドの利用が必要になるかもしれません。
reflectパッケージで使えるリソースについてはこちらで紹介されています。
https://qiita.com/nirasan/items/b6b89f8c61c35b563e8c
reflectを使ったコードは他と比較してコードを難読化させてしまいますが、ユーティリティ用の汎用メソッドのような局所的な利用においては効果を発揮する場面が多いかと思います。
汎用メソッドを実装する際の一つの指針になれば幸いですm(_ _)m