はじめに
Goのスライスを使う上での注意点をよく忘れるので、自分でまとめました。appendやcopyなどを何も考えずに行うと、メモリリークが起きたり、意図しない動作になる可能性があります。また、v1.21以降では、slicesパッケージが追加されたので、そちらについても少し触れております。
スライドも作成したので、載せておきます。
例題
さて、突然ですが以下のコードを実行するとどうなると思いますか?
func main() {
slice1 := make([]int, 0, 5)
slice2 := slice1
for i := 0; i < 10; i++ {
slice1 = append(slice1, i)
slice2 = append(slice2, i+100)
}
fmt.Println("slice1 =", slice1)
fmt.Println("slice2 =", slice2)
}
実行すると、以下のような結果になります。
slice1 = [100 101 102 103 104 5 6 7 8 9]
slice2 = [100 101 102 103 104 105 106 107 108 109]
普通だったら、slice1 = [0 1 2 3 4 5 6 7 8 9]
となるのを期待します。
スライスの構造
スライスを作成すると、Goはまず配列を作成します。この配列にはアクセスできません。次に、配列をスライスすると、Goはその既存の配列へのポインターを取得します。
Goのスライスは以下のような構造をしています。
- 配列へのポインタ : 元となる配列へのポインタ。スライスの元の配列の最初の要素を指す
- スライスの長さ
- 容量 : 元となる配列のサイズ
値渡しとポインタ渡し
まず、Goに参照渡しは存在しません。
- 値渡し : コピーして、中身が等しい新しいものを作成する
- ポインタ渡し : ポインタを渡す。ポインタが参照している値が変化すれば、値が変わる
以下の例では、関数の引数s
は値渡しとなるので、アドレスは変更される。しかし、スライスの要素s[0]
はポインタ渡しになるので、アドレスは変更されない。
func main() {
s := []int{1, 2, 3}
fmt.Printf("%p\n", &s) // 0xc000010018
fmt.Printf("%p\n", &s[0]) // 0xc00001a018
someFunc(s)
}
func someFunc(s []int) {
fmt.Printf("%p\n", &s) // 0xc000010030
fmt.Printf("%p\n", &s[0]) // 0xc00001a018
}
Capacityによるスライスの動作の違い
capacity(容量)によって、スライスの動作が少し異なります。
- capacityが足りないとき
- capacityが足りないとき(関数)
- capacityが足りるとき
- capacityが足りるとき(関数)
Capacityが足りない時
Capacityを超えると新しい配列ができ、ポインタの場所が変更される。
ちなみに、新しい配列のCapacityは2倍になります。(要素数が1024を超えると、25%ずつ増える)
func main() {
s := make([]int, 0, 3)
s = append(s, 1, 2)
fmt.Printf("%p\n", &s) // 0xc000010018
fmt.Printf("%p\n", &s[0]) // 0xc00001a018
s = append(s, 3)
fmt.Printf("%p\n", &s) // 0xc000010018
fmt.Printf("%p\n", &s[0]) // 0xc00001a018
s = append(s, 4) // 容量を超える
fmt.Printf("%p\n", &s) // 0xc000010018
fmt.Printf("%p\n", &s[0]) // 0xc00007a000
}
Capacityが足りない時(関数に渡す時)
関数内では新しい基底配列が作られ、関数内で容量を超えた際にさらに新しい配列が作成される。
func main() {
s := []int{1, 2, 3}
fmt.Printf("%p\n", &s[0]) // 0xc00001a018
add(s)
fmt.Println(s) // [1 2 3]
fmt.Printf("%p\n", &s[0]) // 0xc00001a018
}
func add(s []int) {
fmt.Printf("before: %p\n", &s[0]) // before: 0xc00001a018
s = append(s, 4)
fmt.Println(s) // [1 2 3 4]
fmt.Printf("after: %p\n", &s[0]) // after: 0xc00007e000
}
Capacityが足りてる時
capacityが足りていると、新しい基底配列は作成されず、ポインタは変わらない。
func main() {
s := make([]int, 3, 4)
s[0], s[1], s[2] = 1, 2, 3
fmt.Printf("%p\n", &s) // 0xc0000a8000
fmt.Printf("%p\n", &s[0]) //0xc0000aa000
fmt.Println(len(s)) // 3
s = append(s, 4)
fmt.Printf("%p\n", &s) // 0xc0000a8000
fmt.Printf("%p\n", &s[0]) //0xc0000aa000
fmt.Println(len(s)) // 4
}
Capacityが足りてる時(関数に渡す時)
capacityが足りていると、新しい基底配列は作成されず、ポインタは変わらない。
func main() {
s := make([]int, 3, 4)
s[0], s[1], s[2] = 1, 2, 3
fmt.Printf("%p\n", &s[0]) // 0xc000112000
fmt.Println(len(s)) // 3
add(s)
fmt.Println(s) // [1 2 3]
fmt.Printf("%p\n", &s[0]) //0xc000112000
fmt.Println(len(s)) // 3
}
func add(s []int) {
fmt.Printf("before: %p\n", &s[0]) // before: 0xc000112000
fmt.Println(len(s)) // 3
s = append(s, 4)
fmt.Println(s) // [1 2 3 4]
fmt.Printf("after: %p\n", &s[0]) // after: 0xc000112000
fmt.Println(len(s)) // 4
}
スライス化
配列、もしくはスライスからスライスを作成することをスライス化といいます。
例えば、slice2 := slice1[start:end]
とすると、slice2はslice1のstart番目
〜 end-1番目
で構成されます。
func main() {
slice1 := make([]int, 3, 6)
slice2 := slice1[1:3]
}
slice1とslice2は、下記の図のように同じ配列を参照します。
なので、slice1を変更すると、slice2にも変更が反映されます。
func main() {
slice1 := make([]int, 3, 6)
slice2 := slice1[1:3]
slice1[1] = 1
slice1[2] = 2
fmt.Println("slice1 =", slice1) // slice1 = [0 1 2]
fmt.Println("slice2 =", slice2) // slice2 = [1 2]
}
図にすると、slice1[2] = 3
によって元の配列の要素が更新され、同じ配列を参照しているslice2にも反映されるということ。
次に、slice2に要素を追加します。
以下のようにCapacityを超えない場合は特に問題ないです。
func main() {
slice1 := make([]int, 3, 6)
slice2 := slice1[1:3]
slice1[1] = 1
slice1[2] = 2
slice2 = append(slice2, 3)
slice2 = append(slice2, 4)
slice2 = append(slice2, 5)
fmt.Println("slice1 =", slice1) // slice1 = [0 1 2]
fmt.Println("slice2 =", slice2) // slice2 = [1 2 3 4 5]
}
図にすると以下のように、両方とも同じ配列を参照しております。ただし、appendした要素はslice1からは、長さを超えているため、見えておりません。
slice2にCapacityを超えて要素を追加すると、slice2は新しい配列を参照するようになります。
func main() {
slice1 := make([]int, 3, 6)
slice2 := slice1[1:3]
slice1[1] = 1
slice1[2] = 2
slice2 = append(slice2, 3)
slice2 = append(slice2, 4)
slice2 = append(slice2, 5)
fmt.Printf("slice1 : %p\n", &slice1[0]) // 0xc00011c000
fmt.Printf("slice2 : %p\n", &slice2[0]) // 0xc00011c008
slice2 = append(slice2, 6)
fmt.Printf("slice1 : %p\n", &slice1[0]) // 0xc00011c000
fmt.Printf("slice2 : %p\n", &slice2[0]) // 0xc000122000 新しい配列を参照
fmt.Printf("%d\n", cap(slice2)) // 10 容量が2倍になる
}
ガベージコレクタ
スライスの容量を超えたとき、新たな配列を作成しますが、参照されなくなった配列はGC(ガベージコレクタ)によって解放される。(ヒープ上にある場合のみ)
スライスの初期化のベストプラクティス
makeであらかじめスライスを初期化するときに、長さと容量を省略するのはNG。容量が0で初期化したスライスに、for文で1000個の要素を追加していくとします。
func main() {
slice := make([]int, 0, 0)
for i := 0; i < 1000; i++ {
slice = append(slice, i)
}
}
参照元の配列の容量を超えるたびに、新しい配列(要素が2倍の配列。要素が1024を超える場合は25%ずつ増加)が作られます。
上記の場合だと、0→1→2→4→8→16→32→64→128→256→512→848→1280のように合計13個の配列が作成されます。つまり、12個の配列は不要な配列なので、GCで解放しなければなりません。
以下のように最初に容量を指定すれば、余計な配列のコピーとGCの追加処理の必要性がなくなります。
func main() {
slice := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
slice = append(slice, i)
}
}
また、長さを変更しても良い。
func main() {
slice := make([]int, 1000)
for i := 0; i < 1000; i++ {
slice[i] = i
}
}
上記の要素もしくは、長さを前もって決めておく2つの方法で、パフォーマンスは長さを決めておく方法である。しかし、appendする方法は可読性が高く、多くの場面で好まれる。
nilスライス v.s. 空スライス
- 長さが0なら、スライスは空
- nilと同値ならスライスはnil
空スライスとnilスライスの書き方は以下の4通り存在する。
var slice1 []int
slice2 := []int(nil)
slice3 := []int{}
slice4 := make([]int, 0)
そこで以下のコードを実行すると
func main() {
var slice1 []int
slice2 := []int(nil)
slice3 := []int{}
slice4 := make([]int, 0)
fmt.Printf("empty=%t\tnil=%t\n", len(slice1) == 0, slice1 == nil)
fmt.Printf("empty=%t\tnil=%t\n", len(slice2) == 0, slice2 == nil)
fmt.Printf("empty=%t\tnil=%t\n", len(slice3) == 0, slice3 == nil)
fmt.Printf("empty=%t\tnil=%t\n", len(slice4) == 0, slice4 == nil)
}
1~4の書き方は全て空スライスであり、1,2はnilスライスでもあるということがわかる。
empty=true nil=true
empty=true nil=true
empty=true nil=false
empty=true nil=false
nilスライスは空スライスの一部である。スライスに要素が含まれているかどうかはlen(0)
で判別する。
nilスライスと違い、空スライスは割り当てが発生するので、関数がスライスを返す場合はnilスライスを返す方がよい。
シンタックスシュガーとして使う場合は、2つ目の[]int(nil)
を使う方がいい。
- var s []int
- append(s, 2)
+ s := append([]int(nil, 2)
のように一行で書くことができる。
ちなみに3つ目slice3 := []int{}
は初期要素がある場合に使われる書き方。(e.g. []int{1,2,3}
)
ライブラリによっては、nilスライスと空スライスを区別する。encodeing/jsonでは、nilスライスをマーシャルすると、nullになり、空スライスをマーシャルすると、[]になる。他にもreflect.DeepEqualはnilスライスと空スライスを比較するとfalseになる。
スライスのcopy
間違ったやり方
copy()でスライスをコピーしようとすると、正しく実行されません。
func main() {
slice := []int{0, 1, 2}
var dst []int
copy(dst, slice)
fmt.Println(dst) // []
}
copyは、コピー元のスライス、コピー先のスライスの要素数が少ない方に長さを合わせます。上記の場合だとdstの長さが0、sliceの長さが3なので、最終的にdstは0になります。
解決策1
コピー先のスライスの長さを前もって、コピー元のスライスと同じ長さに設定しておく。
func main() {
slice := []int{0, 1, 2}
dst := make([]int, len(slice))
copy(dst, src)
fmt.Println(dst)
}
解決策2
appendを使う。
func main() {
slice := []int{0, 1, 2}
dst := append([]int(nil), slice...)
}
解決策3(1.21以降)
slices.Clone()を使用する
func main() {
slice := []int{0, 1, 2}
dst := slices.Clone(slice)
fmt.Println(dst) // [0 1 2]
}
スライスのappendの副作用
func main() {
s1 := []int{0, 1, 2}
s2 := s1[1:2]
s3 := append(s2, 10)
fmt.Println(s1) // [0 1 10]
fmt.Println(s2) // [1]
fmt.Println(s3) // [1 10]
}
s1にも変更が影響されてしまう。
同様に関数の場合でも同じ現象が起こります。
func main() {
s := []int{0, 1, 2}
f(s[:2])
fmt.Println(s)
}
func f(s []int) {
_ = append(s, 10) // [0 1 10]
}
解決策1
事前にコピーしておく。
func main() {
s := []int{0, 1, 2}
sCopy := make([]int, 2)
copy(sCopy, s)
f(sCopy)
fmt.Println(s) // [0 1 2]
}
func f(s []int) {
_ = append(s, 10)
}
しかし、このコピーによる解決策は、無駄なコピーが発生するのでよくありません。
解決策2
完全スライス式(s[low:high:max])を渡すことで、解決します。
func main() {
s := []int{0, 1, 2}
f(s[:2:2]) // 完全スライス式を渡す
fmt.Println(s) // [0 1 2]
}
func f(s []int) {
_ = append(s, 10)
}
s[:2:2]にappendするときはcapacityを超えるので、新しい配列を参照します。ゆえに元の配列に影響を起こしません。
バッファとしてスライスを用いる
スライスを活用する方法として、ファイルなどの外部リソースからデータを読み込むためのバッファとして使用することがある。
file, err := os.Open(fileNmae)
if err != nil {
return err
}
defer file.Close()
data := make([]byte, 100)
for {
count, err := file.Read(data)
if err != nil {
return err
}
if count == 0 {
return nil
}
process(data[:count]) // 読み込んだデータの処理
スライスとメモリリーク
既存のスライスや配列をスライス化すると、メモリリークが発生する可能性があります。特に上記のようにバッファとしてスライスを使用するときには、注意しなければなりません。以下のコードは、10バイトのメッセージを1000回受け取って、最初の5バイトを保存するコードです。
func main() {
for i := 0; i < 1000; i++ {
message := []byte{99, 99, 99, ・・・・ , 99} // 1MB
store(getMessageHead(message))
}
}
func getMessageHead(msg []byte) []byte {
return msg[:5]
}
このコードを実行したときに、約1GBのメモリが消費されます。
解決策
messageをスライス化するのではなく、スライスのコピーを作成する。
func getMessageHead(msg []byte)[]byte{
msgHead := make([]byte, 5)
copy(msgHead, msg)
return msgHead
}
値のスライス v.s. ポインタのスライス
[]Tか[]*Tのどっちがいいのか結論をいうと、ほとんどの場合は[]Tの方が優れている。
スライス TIP
v1.21以上ではslices - Go Packages のスライスパッケージを使うことで簡単に導入できます。
削除(Delete)
1.22以降
slices.Delete()で安全にできる。
func main() {
s1 := []int{1, 2, 3, 4}
s2 := slices.Delete(s1, 0, 1)
fmt.Println(s1) // [2 3 4 0]
fmt.Println(s2) // [2 3 4]
}
1.21のとき
slices.Delete()はできるが、元のスライスで削除後に元々あった4, 5番目の要素が残ってしまう。
func main() {
s1 := []int{1, 2, 3, 4}
s2 := slices.Delete(s1, 0, 1)
fmt.Println(s1) // [2 3 4 4]
fmt.Println(s2) // [2 3 4]
}
1.21以前
appendで消せる。
s = append(s[:i], s[i+1:]...)
copyを使ってもできる
s = s[:i+copysa[i:], s[i+1:])]
appendの方はアロケーションが発生する。
また、GCでメモリを解放したい場合は以下のようにする。
if i < len(s)-1 {
copy(s[i:], s[i+1:])
}
s[len(s)-1] = nil // ゼロ値 or nil
s = s[:len(s)-1]
複数削除
1.21以降
先ほどの例と同様slice.Delete()でできる。1.21はgcで解放されない。
1.21以前
s = append(s[:i], s[j:]...)
gcで解放する場合
copy(s[i:], s[j:])
for k, n := len(s)-j+i, len(s); k < n; k++ {
s[k] = nil // nil or ゼロ値
}
s = s[:len(s)-j+i]
削除(順番が担保されない)
s[i] = s[len(s)-1]
s = a[:len(s)-1]
gcで解放する場合
if i < len(s)-1 {
copy(s[i:], s[i+1:])
}
s[len(s)-1] = nil // nil or ゼロ値
s = s[:len(s)-1]
挿入(Insert)
1.21以降
s := []int{1, 2, 3}
s = slices.Insert(s, 0, 0) // 先頭に追加
fmt.Println(s) // [0 1 2 3]
1.21以前
s = append(s, x)
複数挿入
1.21以降
s := []int{1, 2, 3}
s = slices.Insert(s, 2, 8, 7, 6)
fmt.Println(s) // [1 2 8 7 6 3]
1.21以前
s = append(s[:i], append(make([]T, j), s[i:]...)...)
スライスの挿入
a = append(a[:i], append(b, a[i:]...)...)
スライス同士の結合
1.22以降
slice.Concat()
を使うと安全に結合できる。
s3 = slice.Concat(s1,s2,s3)
1.22以前
以下は真っ先に思いつく書き方である。
s3 := append(s1, s2...)
しかし、以下の例ではs3
を書き換えたらs1
まで書き変わってしまいます。
func main() {
s1 := make([]int, 2, 5) // s1 has capacity of 5
s1[0], s1[1] = 2, 3
s2 := []int{7, 8}
s3 := append(s1, s2...)
fmt.Println(s1, s3) // [2 3] [2 3 7 8]
s3[0] = 5
fmt.Println(s1, s3) // [5 3] [5 3 7 8]
}
そこで以下のようにappendすると、安全に結合できます。
s3 := append(s1[:len(s1):len(s1)], s2...)
1.18以上であれば、ジェネリックスを使って定義しておくことができます。
func concatSlice[T any](first []T, second []T) []T {
n := len(first)
return append(first[:n:n], second...)
}
以下は複数スライスの結合の汎用関数です。
func concatMultipleSlices[T any](slices [][]T) []T {
var totalLen int
for _, s := range slices {
totalLen += len(s)
}
result := make([]T, totalLen)
var i int
for _, s := range slices {
i += copy(result[i:], s)
}
return result
}
例題
以下の挙動はどうなるでしょう?
func main() {
slice1 := make([]int, 3, 5)
slice2 := slice1[1:4]
for i := 0; i < 10; i++ {
slice1 = append(slice1, i)
slice2 = append(slice2, i+100)
}
fmt.Println("slice1 =", slice1)
fmt.Println("slice2 =", slice2)
}
実行結果
slice1 = [0 0 0 0 1 2 3 4 5 6 7 8 9]
slice2 = [0 0 0 1 101 102 103 104 105 106 107 108 109]