スライスの内部実装と挙動の確認
スライスの挙動でなんでこうなるんだ?と度々なるのでまとめてみる。
配列
まず配列について。
配列は長さを含めて型。
An array’s size is fixed; its length is part of its type ([4]int and [5]int are distinct, incompatible types).
長さ4の配列は以下のような感じで
var arr [4]int
メモリ内では、4つの整数値を順番に並べて表現される。
Goの配列は値であり、配列を格納した変数は配列全体を表す。(C言語では配列の最初の要素へのポインタ)
Go’s arrays are values. An array variable denotes the entire array; it is not a pointer to the first array element (as would be the case in C).
スライスの基本
スライスの定義
スライスは配列へのポインタと、長さと容量を持った値。
type slice struct { array unsafe.Pointer len int cap int }
スライスは内部配列のポインタを参照しているので、関数に渡すときは値渡しでもポインタ渡しでも書き換えが可能。
初期化
以下のような形で初期化することが多い。
s1 := make([]int, 0, 5) // 内部配列のサイズが5で値が入っていない(ように見える)スライス(内部配列には[0 0 0 0 0]で初期化されているはず) fmt.Println(s) // [] s2 := make([]int, 5) // 内部配列のサイズも値の入っているサイズも5のスライス fmt.Println(s) // [0 0 0 0 0]
インデックス操作
コロン:
で区切ることでslicing
することができる。
例えば、b[1:4]
はスライスb
のインデックス 1 から 3(紛らわしいけど4は含まない) までの要素を含むスライスを作成する 。
s := []int{0, 1, 2, 3, 4} s1 := s[1:4] fmt.Println(s1, len(s1), cap(s1)) // [1 2 3] 3 4
容量は切り取ったスライスの先頭から参照している内部配列の容量までを数える。
The capacity is the number of elements in the underlying array (beginning at the element referred to by the slice pointer).
再度slicing
すれば容量まで大きくすることができる。
s := []int{0, 1, 2, 3, 4} s1 := s[1:4] fmt.Println(s1, len(s1), cap(s1)) // [1 2 3] 3 4 s2 := s1[:cap(s1)] fmt.Println(s2, len(s2), cap(s2)) // [1 2 3 4] 4 4
このようにslicing
して値が一見見えなくても内部配列では値を保持しており、 GC されず値が残る場合がある。
かなりの要素数をslicing
するようであれば注意したい。
Since the slice references the original array, as long as the slice is kept around the garbage collector can’t release the array
要素の追加
append()
はそのスライスから見た末尾へ値を追加(末尾+1の位置を書き換え)しスライスの長さを更新する。
内部配列の長さが足りない場合は、まず新しく長さが倍の配列を用意してそこに既存の内部配列の値をコピーし、末尾へ値を追加したのち、その新しい配列を内部配列として参照するようにスライスが持つ配列へのポインタを貼り直して、スライスの長さと容量を更新する。
arr := make([]int, 0, 0) fmt.Println(arr, len(arr), cap(arr)) // [] 0 0 arr = append(arr, 0) fmt.Println(arr, len(arr), cap(arr)) // [0] 1 1 arr = append(arr, 1) fmt.Println(arr, len(arr), cap(arr)) // [0 1] 2 2 arr = append(arr, 2) fmt.Println(arr, len(arr), cap(arr)) // [0 1 2] 3 4 arr = append(arr, 3) fmt.Println(arr, len(arr), cap(arr)) // [0 1 2 3] 4 4 arr = append(arr, 4) fmt.Println(arr, len(arr), cap(arr)) // [0 1 2 3 4] 5 8
コードを見た感じ、容量が256以上だと単純に2倍確保するわけでは無さそう。
https://cs.opensource.google/go/go/+/master:src/runtime/slice.go;l=196
このようにappend
時に容量オーバーしてしまうと、新しいメモリ領域をOSに要求して確保し、既存のデータを新しい領域にコピーする必要がある。
なるべくこのコストを減らすために、事前にmake()
で必要十分量のメモリを確保しておく工夫が必要。
ケーススタディ
シャローコピー時の挙動
シャローコピーした場合は参照している内部配列は同じなので、コピー先はコピー元の影響を受ける(逆も然り)
ただしappend
の際に容量オーバーで内部配列が変わると影響は受けなくなる。
a := []int{1, 2, 3} b := a // シャローコピー(参照している内部配列は同じ) fmt.Println(b) // [1 2 3] a[0] = 111 // aとbは変数自体のメモリのアドレスは別だが、内部配列は同じなのでb[0]も111になる fmt.Println(a) // [111 2 3] fmt.Println(b) // [111 2 3] a = append(a, 4) // 容量オーバーaの内部配列が変わるのでbには影響なし fmt.Println(a) // [111 2 3 4] fmt.Println(b) // [111 2 3]
コピー元のスライスにappend
した時のコピー先の挙動
append
では対象のスライスの長さしか更新されないので、コピー元のスライスにappend
した場合、コピー先が同じ配列を参照していてもコピー先のスライスの長さは更新されていないので、コピー先からは追加された要素が見えない。
a := make([]int, 0, 4) a = append(a, 1, 2, 3) fmt.Println(a) // [1 2 3] b := a a[0] = 111 fmt.Println(a) // [111 2 3] fmt.Println(b) // [111 2 3] a = append(a, 4) // 容量に余裕があるのでaとbは同じ内部配列を参照するが、aのappendではbのスライスの長さは更新されないのでbからは追加された4が見えない fmt.Println(a) // [111 2 3 4] fmt.Println(b) // [111 2 3] fmt.Println(len(b), cap(b)) // 3 4
append
による内部配列の書き換え
append
はスライスから見た末尾に値を追加するので、同じ内部配列を参照しているスライスから見ると書き換えられて見えることがある。
a := make([]int, 0, 4) a = append(a, 1, 2, 3) fmt.Println(a) // [1 2 3] b := a a = append(a, 4) // 内部配列に4を追加してaのスライスの長さを更新 fmt.Println(a) // [1 2 3 4] fmt.Println(b) // [1 2 3] b = append(b, 5) // bのスライスから見た末尾に5を追加(内部配列の4要素目に5を入れる)するので、内部配列の4要素目の4が5に書き換えられる fmt.Println(a) // [1 2 3 5] fmt.Println(b) // [1 2 3 5]
補足: シャローコピーとディープコピー
シャローコピー
単純に別の変数に代入するだけだとシャローコピーなので要素のアドレスは同じ。
シャローコピーは実体(データ)のコピーを行わないで、オブジェクトをコピーすること。
a := []int{1, 2, 3} b := a // シャローコピー fmt.Printf("a at %p, b at %p\n", &a, &b) // 変数自体のアドレスは当然異なる fmt.Printf("a[0] at %p, b[0] at %p\n", &a[0], &b[0]) // 内部配列は同じなので要素のアドレスは同じ
ディープコピー
copy()
を使ってディープコピーする。
a := []int{1, 2, 3} b := make([]int, len(a)) copy(b, a) // aをbにコピー fmt.Printf("a at %p, b at %p\n", &a, &b) // 変数自体のアドレスは当然異なる fmt.Printf("a[0] at %p, b[0] at %p\n", &a[0], &b[0]) // 内部配列が別なので要素のアドレスは異なる