Go言語のキャッチアップ その3 ~スライス~
Goのスライス(Slice)
スライスは、「配列の一部または全部を参照する、可変長のデータ構造。」らしいが、よくわからん。 公式ドキュメント上では、
スライスは基底配列の連続した一部分を表し、その要素にインデックスを使ってアクセスできるもの として説明されている。 The Go Programming Language Specification
これもまだ抽象的に感じるので実際のコードを見る。
package main
import "fmt"
func main() {
numbers := [5]int{10, 20, 30, 40, 50}
s := numbers[1:4]
fmt.Println(s)
// [20 30 40]
}
なるほど。
numbers[1:4]と書くことで、配列のインデックス1から3までの要素を指定している。
つまり「配列のここからここまでをいまから使うぞ!」と指定しているようなもんか。
このnumbers[1:4]は、Goでは「スライス式(slice expression)」と呼ばれるらしい。
処理を整理すると、
- numbersという配列が存在する
- numbers[1:4]で、インデックス1~3の範囲を指定する。
- その範囲を扱うスライスが作成される
- 作られたスライスを変数sに代入する。
ここで、numbersとsは異なる型になっている。
fmt.Printf("%T\n", numbers)
// [5]int
fmt.Printf("%T\n", s)
// []int
numbersは配列、sはスライス。 ただし、スライスは単なる範囲指定の宣言ではなく、実際に作られる値である。 また、スライスは必ずしも既存の配列から作る必要はない。
s := []int{10, 20, 30}
このように書いた場合も、内部では要素を格納する配列が用意される。
スライスは元の配列をコピーしているわけではない。
ここで疑問が一つ浮かんだ。 numbers[1:4]でスライスを作った場合、元の配列からその範囲の値がコピーされているのだろうか? 結論コピーはされていない。
package main
import "fmt"
func main() {
numbers := [5]int{10, 20, 30, 40, 50}
s := numbers[1:4]
s[0] = 999
fmt.Println(s)
// [999 30 40]
fmt.Println(numbers)
// [10 999 30 40 50]
}
このようにスライス側の値を変更すると、元の配列の値も変更されている。 関数の引数として配列を渡す場合、元の配列とは別の配列を関数側でコピーするが、スライスの場合元の配列を参照したままになるため、挙動が異なる。
配列の場合は、
a := [3]int{10, 20, 30}
b := a
b[0] = 999
fmt.Println(a) // [10 20 30]
fmt.Println(b) // [999 20 30]
このように代入時に配列全体がコピーされるため、bを変更してもaには影響しなかった。 しかしスライスは異なり、配列の要素をコピーするのではなく、元の配列のメモリ領域を参照している。 そのため、同じデータを操作することになる。
スライスと配列は相互に参照しているのか?
スライス側は元の配列を参照しているが、配列側はスライスを参照するのか? 結論NOで、参照しているのはスライスから配列の一方向のみ。
スライスの長さ(len)と容量(cap)について。
スライスには長さと容量という概念があり、それぞれlenとcapと呼ぶ。 調べてみると、
- len: スライスからアクセスできる要素数
- cap: スライスの先頭から基底配列の末尾までの容量
という説明が出てきた。「アクセスできる要素数」ってなんやねん。
もっと簡単に言えば「lenはスライスに含まれる要素の数」ということらしい。普通にlengthやな。
numbers := [5]int{10, 20, 30, 40, 50}
s := numbers[1:3]
fmt.Println(s) // [20 30]
fmt.Println(len(s)) // 2
スライスsに含まれているのは20と30の2つだけ。なのでlen(s)は2となる。 ではcapは何なのか。
fmt.Println(cap(s)) // 4
これは4になる。なぜか? 元の配列は [10, 20, 30, 40, 50] で、スライスの開始位置はインデックス1, つまり20の位置。 そこから元の配列の末尾までを数えると、 20, 30, 40, 50の4つとなる。 だからcap(s)は4となる。
スライスの範囲を拡張する
numbers := []int{10, 20, 30, 40, 50}
s := numbers[1:4]
// [20 30 40]
fmt.Println(s[3])
// panic: runtime error: index out of range
現在のスライス s のインデックスは0〜2までなので、s[3] にアクセスするとpanicになる。 このように範囲外のエラーとなる。
スライスは容量の範囲内であれば、扱う範囲を拡張できる。
s = s[:4]
このようにすれば、スライスの先頭からindex4の手前まで、つまり4要素分を指定して新しいスライスを作ることができる。 スライスsを先頭から4要素分まで拡張するで、という感じ。
package main
import "fmt"
func main() {
numbers := []int{10, 20, 30, 40, 50}
s := numbers[1:4]
fmt.Println(s) // [20 30 40]
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 4
// スライスを4要素分に拡張
s = s[:4]
fmt.Println(s) // [20 30 40 50]
fmt.Println(len(s)) // 4
fmt.Println(cap(s)) // 4
fmt.Println(s[3]) // 50
}
まとめ
-
スライスは配列そのものではない。配列のどの範囲を扱うかという情報を持つもの。
-
スライスは元の配列の要素をコピーしない。基底配列のメモリ領域を参照しているため、スライス側で値を変更すると元の配列にも反映される。
-
参照関係は一方向。スライスは基底配列を参照しているが、配列側はスライスの存在を知らない。
-
lenは現在スライスに含まれている要素数。
-
capはスライスの開始位置から、基底配列の末尾までの容量。
-
スライスはcapの範囲内であれば拡張できる。ただし、要素を新しく追加しているわけではなく、基底配列に存在する要素を扱える範囲を広げているだけ。