Go言語 スライスのchunk関数をつくる

スライスを任意の個数毎に分割して処理したい場合がある。
汎用性を、考えてどんな型のスライスでも受け取れるようにしようとして、
[]interface{} を扱おうとすると、[]int や、[]string, []T のようなスライスを直接渡せない事に気付く。

[]interface{} と、[]T は、メモリ上の表現が違うので、そういった事が出来ないようだ。
https://golang.org/doc/faq#convert_slice_of_interface

Go1にはまだジェネリクスがないので、
おそらくこの問題は解決できないようです。

スライスのchunk関数

今回の場合は、スライスを渡すのではなく
スライスのインデックスを生成してくれる関数を作る事で、汎用的に書くことができます。

// 添字のfrom, to を表現する構造体を用意
type IndexChunk struct {
    From, To int
}

func IndexChunks(length int, chunkSize int) <-chan IndexChunk {
    ch := make(chan IndexChunk)

    go func() {
        defer close(ch)

        for i := 0; i < length; i += chunkSize {
            idx := IndexChunk{i, i + chunkSize}
            if length < idx.To {
                idx.To = length
            }
            ch <- idx
        }
    }()

    return ch
}

この関数を利用する際は、for range でループし、From, To を使って添字アクセスすれば良い。

// []T どんな型スライスでもOK
s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}

// 任意の個数
n := 4

for idx := range IndexChunks(len(s), n) {
    // From, To を使って添字アクセスする
    fmt.Println(s[idx.From:idx.To])
}

//===> [1 2 3 4]
//===> [5 6 7 8]
//===> [9]

[]interface{} では、[]Tは渡せない

先に結論を書いてしまいましたが、
普通に実装しようとした場合、自分も下記のような関数を作って処理しようとしていました。

// []interface{} だから、どんな型のスライスでも渡せるんじゃない??
func SliceChunks(s []interface{}, chunkSize int) <-chan []interface{} {
    ...
}

[]interface{} なら、もちろんそのまま引数に渡して使うことができます。

// []interface{} 型で定義してるからOK
s := []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9}

for chunk := range SliceChunks(s, 4) {
    fmt.Println(chunk)
}

//===> [1 2 3 4]
//===> [5 6 7 8]
//===> [9]

しかし、[]int, []string, []T のように定義されたものは渡すことができません。

// []T 型で定義した場合はNG
ints := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}

// コンパイルエラー
for chunk := range SliceChunks(ints, 4) {
    fmt.Println(chunk)
}

cannot use ints (type []int) as type []interface {} in argument to SliceChunks

ほかの解決方法

最初に見せた、IndexChunks関数を使う事をおすすめしますが、
一応、他の実装をするのであれば、以下のような方法があります。

[]interface{}型にコピーして、関数にわたす。

[]interface{} と、[]Tはメモリ表現が違うため直接渡せないので、
新たにスライスを作成してコピーして使う。

// []interface{}にコピーする
copiedSlice := make([]interface{}, len(s))
for i, v := range s {
    copiedSlice[i] = v
}

// コピーしたスライスを渡す
for chunk := range SliceChunks(copiedSlice, 4) {
    // chunkは、[]interface{} なので、元の[]Tで扱いたい場合、再度コピーしないといけない。。
    fmt.Println(chunk)
}

わざわざコピーするのが面倒ですね。

型に合わせて、それぞれに実装関数を作る。

汎用性は諦めて、[]T型に合わせた関数をそれぞれ用意しちゃうというもの。
関数を使う範囲が限られているのであれば、冗長ですがコレでいいと思います。

[]interface{} じゃなくて、interface{} という引数にしてしまう。

interface{} であれば、本当にどんな値でも渡すことができます。
それが、スライスであってもOKということです。
本当にスライスかどうかはリフレクションを使って、判定して処理しちゃう。

関数の戻り値も、interface{} になるので、型アサーションして扱う事になる。
何を渡したらいいかよくわからん関数になってしまいますし、コンパイル時に変な使い方してるかのチェックもできない。

あんまりおすすめしない方法ですね。

コメントを残す