メモリアロケーションなしで slice をフィルタする

令和最初のエントリです。
連休前に教えてもらったことについてまとめました。

このエントリに記載する内容については、githubSliceTricks · golang/go Wiki · GitHub に記載されてる内容になります。

go の sliceについて

go の slice はポインタ型です。また、go で slice を初期化する際には、通常はメモリアロケーションが走ります。

// sample
a := []int{1, 2, 3}
b := make([]string, 0, 0)

go の slice については Go Slices: usage and internals - The Go Blog この辺が詳しいです。

slice をメモリアロケーションなしでフィルタする

ある slice から特定の値を持ってるものだけを抜き出し、別の slice にコピーするようなユースケースを考えてみます。
素直にコードを書くと以下のようになると思います。

func main() {
    arr := []string{"apple", "banana", "orange"}
    arr2 := make([]string, 0, 0)
    for _, v := range arr {
        v := v
        if v == "apple" {
            arr2 = append(arr2, v)
        }

    }

    fmt.Printf("arr:%v, address: %p\n", arr, arr)
    fmt.Printf("arr2:%v, address: %p\n", arr2, arr2)
}

// 出k力結果
// arr:[apple banana orange], address: 0x43e260
// arr2:[apple], address: 0x40c128

通常、こういったある slice の中から特定の値を取り出した slice を作り直したい(slice の値を詰め直す)ときは、別に詰め直す用の slice をインスタンス化しておいて、loopで回して詰め直すのが一般的な方法だと思います。

しかし、この方法だと、ソースとなる slice と詰め直す先の slice で二重にメモリアロケーションが必要になります。
※ 出力結果でもアドレスの値は異なります。

しかし、現実世界でプロダクトを運用しているとメモリが厳しく、slice 一つとっても極力メモリ空間を使いまわして、メモリアロケーションを走らせることなく slice をフィルタリングしたいユースケースってあると思います。ソースの slice で使用されてるメモリをうまいこと再利用して slice を詰め直す方法が SliceTricks · golang/go Wiki · GitHub に書いてありました。

この gowiki に記載されてる Tips を元に実際のサンプルコードを書き直してみます。

func main() {
    arr := []string{"apple", "banana", "orange"}
    arr2 := arr[:0] // ここが異なる。
    for _, v := range arr {
        v := v
        if v == "apple" {
            arr2 = append(arr2, v)
        }

    }

    fmt.Printf("arr:%v, address: %p\n", arr, arr)
    fmt.Printf("arr2:%v, address: %p\n", arr2, arr2)
}

// 出力内容
// arr:[apple banana orange], address: 0x43e260
// arr2:[apple], address: 0x43e260

ref: https://play.golang.org/p/D9ubw_22WTw

出力を確認すると、詰め直した先の slice もソースとなった slice のアドレスと同じなので、うまくメモリを再利用できています。

注意点も gowiki には記載してあります。

This trick uses the fact that a slice shares the same backing array and capacity as the original, so the storage is reused for the filtered slice. Of course, the > original contents are modified.

以下のような slice の各要素の値を変更する場合、同じアドレスで詰め替えを行うので元のソースの slice も書き換えてしまいます。

func main() {
    a := []string{"a", "b", "c"}
    b := a[:0]
    fmt.Printf("a: %[1]p = %+[1]v\n", a)

    for _, aa := range a {
        aa := aa
        b = append(b, fmt.Sprintf("1-%s", aa))
    }

    fmt.Printf("b: %[1]p = %+[1]v\n", b)
    fmt.Printf("a: %[1]p = %+[1]v\n", a)
}


// 出力結果
// a: 0x43e260 = [a b c]
// b: 0x43e260 = [1-a 1-b 1-c]
// a: 0x43e260 = [1-a 1-b 1-c]

ref: https://play.golang.org/p/LxYnXCUNpjP