どのようなプログラミング言語を利用していても、可変長配列はよく使います。
Go 言語においては、 slice
という可変長配列が用意されています。
slice
を用意するためには make
関数を利用します。
length := 5
capacity := 10
array := make([]int, length, capacity)
make
関数の第 1 引数([]int
)が型、第 2 引数(length
)が 長さ
、第 3 引数(capacity
)が 容量
を意味しています。
今回は、 長さ
と 容量
のそれぞれが、どのような役割なのかを、サンプルコードをつかって確認しました。
準備
slice の長さと容量を求める関数
slice
の 長さ
は len
関数、 容量
は cap
関数で求めることが出来ます。
-
サンプルコード1:
package main import "fmt" func main() { lenght := 5 capacity := 10 array := make([]int, lenght, capacity) fmt.Printf("初期の長さ -> %d\n", len(array)) fmt.Printf("初期の容量 -> %d\n", cap(array)) }
-
出力結果:
初期の長さ -> 5 初期の容量 -> 10
今後のサンプルコードでも
len
関数とcap
関数を使って様子を観測します。
slice の各要素の初期値を観測する
slice
の初期値を、添字を使って確認します。
長さ分の初期値
-
サンプルコード2:
package main import "fmt" func main() { lenght := 5 capacity := 10 array := make([]int, lenght, capacity) fmt.Printf("初期の長さ -> %d\n", len(array)) fmt.Printf("初期の容量 -> %d\n", cap(array)) fmt.Println() fmt.Println("長さ分の各要素") for i := 0; i < len(array); i++ { fmt.Printf("array[%d] -> %d\n", i, array[i]) } }
-
出力結果:
初期の長さ -> 5 初期の容量 -> 10 長さ分の各要素 array[0] -> 0 array[1] -> 0 array[2] -> 0 array[3] -> 0
長さ分の要素には、
int
型の初期値である0
が代入されています。
容量分の初期値
-
サンプルコード3:
package main import "fmt" func main() { lenght := 5 capacity := 10 array := make([]int, lenght, capacity) fmt.Printf("初期の長さ -> %d\n", len(array)) fmt.Printf("初期の容量 -> %d\n", cap(array)) fmt.Println() fmt.Println("容量分の各要素") for i := 0; i < cap(array); i++ { fmt.Printf("array[%d] -> %d\n", i, array[i]) } }
-
出力結果:
初期の長さ -> 5 初期の容量 -> 10 容量分の各要素 array[0] -> 0 array[1] -> 0 array[2] -> 0 array[3] -> 0 array[4] -> 0 panic: runtime error: index out of range goroutine 1 [running]: main.main() /home/vagrant/go/src/sample/sample3.go:16 +0x49c goroutine 2 [runnable]: runtime.forcegchelper() /usr/local/go/src/runtime/proc.go:90 runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:2232 +0x1 goroutine 3 [runnable]: runtime.bgsweep() /usr/local/go/src/runtime/mgc0.go:82 runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:2232 +0x1 goroutine 4 [runnable]: runtime.runfinq() /usr/local/go/src/runtime/malloc.go:712 runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:2232 +0x1 exit status 2
長さ
以上の要素に、添字を使ってアクセスした時にpannic
が発生しています。これで添字でアクセス出来るのは、長さ分までということが分かりました。
slice を可変長配列として利用する
slice
に要素を追加するには append
関数を利用します。
append
関数で要素を追加すれば、可変長配列として slice
を利用できます。
容量の変化
容量に収まる様に要素を追加したときの変化
-
サンプルコード4:
package main import "fmt" func main() { lenght := 5 capacity := 10 array := make([]int, lenght, capacity) fmt.Printf("初期の長さ -> %d\n", len(array)) fmt.Printf("初期の容量 -> %d\n", cap(array)) appendCount := 3 for i := 0; i < appendCount; i++ { array = append(array, i+1) } fmt.Println() fmt.Printf("append 後の長さ -> %d\n", len(array)) fmt.Printf("append 後の容量 -> %d\n", cap(array)) fmt.Println() fmt.Println("append 後の長さ分の各要素") for i := 0; i < len(array); i++ { fmt.Printf("array[%d] -> %d\n", i, array[i]) } }
-
出力結果:
初期の長さ -> 5 初期の容量 -> 10 append 後の長さ -> 8 append 後の容量 -> 10 append 後の長さ分の各要素 array[0] -> 0 array[1] -> 0 array[2] -> 0 array[3] -> 0 array[4] -> 0 array[5] -> 1 array[6] -> 2 array[7] -> 3
append した分、初期の長さ分の後に追加されています。
容量
は変化しないことが分かりました。
容量以上になる様に要素を追加したときの変化
-
サンプルコード5:
package main import "fmt" func main() { lenght := 5 capacity := 10 array := make([]int, lenght, capacity) fmt.Printf("初期の長さ -> %d\n", len(array)) fmt.Printf("初期の容量 -> %d\n", cap(array)) appendCount := 6 for i := 0; i < appendCount; i++ { array = append(array, i+1) } fmt.Println() fmt.Printf("append 後の長さ -> %d\n", len(array)) fmt.Printf("append 後の容量 -> %d\n", cap(array)) fmt.Println() fmt.Println("append 後の長さ分の各要素") for i := 0; i < len(array); i++ { fmt.Println(i, "->", array[i]) } }
-
出力結果:
初期の長さ -> 5 初期の容量 -> 10 append 後の長さ -> 11 append 後の容量 -> 20 append 後の長さ分の各要素 0 -> 0 1 -> 0 2 -> 0 3 -> 0 4 -> 0 5 -> 1 6 -> 2 7 -> 3 8 -> 4 9 -> 5 10 -> 6
append した分、初期の長さ分の後に追加されています。
それに加え、容量が初期の
10
から20
に増えています。これにより、初期に確保した容量とは関係なく、要素を追加出来ることが分かりました。
アドレスの変化
slice
のアドレスを観測することにより、容量以上に要素を追加した時の振る舞いを観測します。
容量に収まる様に要素を追加したときの変化
-
サンプルコード6:
package main import "fmt" func main() { lenght := 5 capacity := 10 array := make([]int, lenght, capacity) fmt.Printf("初期のアドレス -> %p\n", array) appendCount := 3 for i := 0; i < appendCount; i++ { array = append(array, i+1) } fmt.Printf("append 後のアドレス -> %p\n", array) }
-
出力結果:
初期のアドレス -> 0xc2080480a0 append 後のアドレス -> 0xc2080480a0
アドレスが同じです。
容量に収まる様に要素を追加したときの変化
-
サンプルコード7:
package main import "fmt" func main() { lenght := 5 capacity := 10 array := make([]int, lenght, capacity) fmt.Printf("初期のアドレス -> %p\n", array) appendCount := 7 for i := 0; i < appendCount; i++ { array = append(array, i+1) } fmt.Printf("append 後のアドレス -> %p\n", array) }
-
出力結果:
初期のアドレス -> 0xc2080480a0 append 後のアドレス -> 0xc20804c000
アドレスが変わっています。
容量以上の要素数を追加するときに、
append
関数内部で領域の再確保が発生し、新しい領域のアドレスが戻り値として返します。データは再確保された領域にコピーされます。データの量が多いと、計算コストがかかります。
容量の増え方
ついでに 容量
の増え方について観測してみます。
-
サンプルコード8:
package main import "fmt" func main() { lenght := 5 capacity := 10 array := make([]int, lenght, capacity) fmt.Printf("初期の長さ -> %d\n", len(array)) fmt.Printf("初期の容量 -> %d\n", cap(array)) fmt.Println() appendCount := 100 for i := 0; i < appendCount; i++ { array = append(array, i+1) fmt.Printf("%3d 個追加後の容量 -> %d\n", i+1, cap(array)) } }
-
出力結果:
初期の長さ -> 5 初期の容量 -> 10 1 個追加後の容量 -> 10 2 個追加後の容量 -> 10 3 個追加後の容量 -> 10 4 個追加後の容量 -> 10 5 個追加後の容量 -> 10 6 個追加後の容量 -> 20 7 個追加後の容量 -> 20 ... 14 個追加後の容量 -> 20 15 個追加後の容量 -> 20 16 個追加後の容量 -> 40 17 個追加後の容量 -> 40 ... 34 個追加後の容量 -> 40 35 個追加後の容量 -> 40 36 個追加後の容量 -> 80 37 個追加後の容量 -> 80 ... 74 個追加後の容量 -> 80 75 個追加後の容量 -> 80 76 個追加後の容量 -> 160 77 個追加後の容量 -> 160 ... 99 個追加後の容量 -> 160 100 個追加後の容量 -> 160
長さ
が容量
を超えた時に、その時の容量
の倍の容量
が新たに確保されることが分かりました。
今回分かったこと
append 関数だけを使って要素を追加していくときには、長さは 0 に指定しておく
append
関数は、既存の要素の最後に新しい要素を追加します。
make
関数で長さを 0
以外の値にしたとき、初期の長さ分の要素を考慮した作りする必要があります。
扱う要素数の検討が付くときには、要素で指定しておく
新しい要素の確保、データのコピーのコストを無視すれば、容量は気にしないい事がわかりました。
しかし、事前に扱う要素素が見当がつく場合は、最初に make
関数で指定しておくほうがいいです。
array := make([]int, 0, capacity)
引数で slice を渡し、関数内で append 関数を利用するときには、必ず戻り値で戻す
要素数が容量を超えたとき、アドレスが新しく割り振られる事がわかりました。
したがって、関数の引数として渡した slice
が、関数内部で appende
関数が使われていた場合、気をつけないと期待した効果が得られません。
-
サンプルコード9:
package main import "fmt" func appendData(a []int) []int { appendCount := 100 for i := 0; i < appendCount; i++ { a = append(a, i+1) } return a } func main() { lenght := 0 capacity := 0 array := make([]int, lenght, capacity) fmt.Printf("初期の長さ -> %d\n", len(array)) fmt.Printf("初期の容量 -> %d\n", cap(array)) fmt.Printf("初期のアドレス -> %p\n", array) newArray := appendData(array) fmt.Println() fmt.Printf("呼出し後の長さ -> %d\n", len(array)) fmt.Printf("呼出し後の容量 -> %d\n", cap(array)) fmt.Printf("呼出し後のアドレス -> %p\n", array) fmt.Println() fmt.Printf("戻り値の長さ -> %d\n", len(newArray)) fmt.Printf("戻り値の容量 -> %d\n", cap(newArray)) fmt.Printf("戻り値のアドレス -> %p\n", newArray) }
-
出力結果:
初期の長さ -> 0 初期の容量 -> 0 初期のアドレス -> 0x5549e0 呼出し後の長さ -> 0 呼出し後の容量 -> 0 呼出し後のアドレス -> 0x5549e0 戻り値の長さ -> 100 戻り値の容量 -> 128 戻り値のアドレス -> 0xc208050000