この記事は2021年Goアドベントカレンダー2 8日目の記事です。
はじめに
Goにはslice(スライス)というデータ構造が提供されています。
可変長配列のようなもので、Goを触ろうとした人の中にはこのスライス操作で躓く人が多いのかもしれません。
Goには他の言語のようにコレクションに対する便利な関数が提供されていないのは有名だと思いますが、基本的にGopherはこのスライスをfor文で愚直にイテレーションしたり append
/ copy
関数を使って要素の追加や削除を行なっていきます。
おそらくですが、Wiki にあるSliceTricks にお世話になった人は多いでしょう。
そんなスライスですが、今回はスライス操作についてではなくスライス式の挙動について簡単に書いていこうと思います。
スライス式(Slice expressions)とは
Slice expressions construct a substring or slice from a string, array, pointer to array, or slice. There are two variants: a simple form that specifies a low and high bound, and a full form that also specifies a bound on the capacity.
スライス式とは [:]
のような表現でスライスの部分的な値を取得できる式のことです。
このスライス式には上限と下限を指定した書き方と、容量まで指定した書き方の2種類の表現が存在します。
Simple slice expressions
a[low:high]
のように上限/下限を指定して、部分的な値を取り出す表現です。
まずは以下に []int
を使ったコードを提示します。
package main
import "fmt"
func main() {
a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // len: 10のintのスライス
fmt.Println(a[:]) // = a[0:len(a)] = [0 1 2 3 4 5 6 7 8 9]
fmt.Println(a[3:5]) // = [3 4]
fmt.Println(a[3:3]) // = []
fmt.Println(a[:5]) // = a[0:5] = [0 1 2 3 4]
fmt.Println(a[3:]) // = a[3:len(a)] = [3 4 5 6 7 8 9]
}
この low/high
にはいくつかの省略方法があり、 例の通り a[0:10]
は a[:10]
のように書くこともできます。省略した場合、暗黙的に low=0, high=len(a) となります。
low/high
は以下の制約の数値(int) を設定することができます。
- 配列の場合: 0 <= low <= high <= len(a) (※aは任意の配列)
- スライスの場合: 0 <= low <= high <= cap(a) (※aは任意のスライス)
上記の通り、 配列の場合は要素数分まで指定することができますが、スライスの場合は要素数ではなく最大容量まで指定することができます。
試しにコードを書いてみます。以下は先ほど提示したコードを少し改造したものです。
package main
import "fmt"
func main() {
a := make([]int, 10, 50) // len: 10, cap: 50 のintのスライス
// 深い意味はないが [0 1 2 3 4 5 6 7 8 9] みたいにしておく
for i := 0; i < 10; i++ {
a[i] = i
}
fmt.Println(a[:]) // = a[0:len(a)] = [0 1 2 3 4 5 6 7 8 9]
fmt.Println(a[3:5]) // = [3 4]
fmt.Println(a[3:3]) // = []
fmt.Println(a[:5]) // = a[0:5] = [0 1 2 3 4]
fmt.Println(a[3:]) // = a[3:len(a)] = [3 4 5 6 7 8 9]
fmt.Println(a[:50]) // = [0 1 2 3 4 5 6 7 8 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
}
無事、 capの上限までスライス式で取得することができました。 また、 a[:]
の場合は、capではなくlenで取得されていることは(仕様通りですが)少し面白いですね。
Full slice expressions
[Simple slice expressions](#Simple slice expressions) で少しcapの話をしましたが、 Full slice expressions
は capに関する指定を行う表現です。
a[low : high : max]
のように記述し、 Simple slice expressions
に対して、一つ分 :
が増えています。
low/high/max
は 0 <= low <= high <= max <= cap(a)
の範囲で指定することができ、 この際に max-low
分のcap
が確保されます。
また Simple slice expressions
と違い、 highの省略ができないようになっています。
Simple slice expressions
時と同様に []int
で挙動を見てみることにします。
package main
import "fmt"
func main() {
a := make([]int, 10, 50) // len: 10, cap: 50 のintのスライス
// 深い意味はないが [0 1 2 3 4 5 6 7 8 9] みたいにしておく
for i := 0; i < 10; i++ {
a[i] = i
}
// a[5::50]
fmt.Printf("slice: %v, cap: max-low = 15-0 = %d\n", a[:len(a):15], cap(a[:len(a):15])) // slice: [0 1 2 3 4 5 6 7 8 9], cap: max-low = 15-0 = 15
fmt.Printf("slice: %v, cap: max-low = 10-3 = %d\n", a[3:5:10], cap(a[3:5:10])) // slice: [3 4], cap: max-low = 10-3 = 7
fmt.Printf("slice: %v, cap: max-low = 5-3 = %d\n", a[3:3:5], cap(a[3:3:5])) // slice: [], cap: max-low = 5-3 = 2
// 省略できるのはlowのみなので、以下はエラーになる。
// fmt.Println(a[4::len(a)]) // middle index required in 3-index slice
// fmt.Println(a[4:5:]) // final index required in 3-index slice
// fmt.Println(a[4::]) // final index required in 3-index slice
fmt.Printf("slice: %v, cap: max-low = 50-0 = %d\n", a[:30:50], cap(a[:30:50])) // slice: [0 1 2 3 4 5 6 7 8 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], cap: max-low = 50-0 = 50
}
仕様通り a[low:high:max]
としたときに max-low
が cap
になっていることがわかります。
Simple slice expressionsの時にはどうなるのか
暗黙的に max=cap(a)
となります。
package main
import "fmt"
func main() {
a := make([]int, 10, 50) // len: 10, cap: 50 のintのスライス
// 深い意味はないが [0 1 2 3 4 5 6 7 8 9] みたいにしておく
for i := 0; i < 10; i++ {
a[i] = i
}
fmt.Printf("slice: %v, cap: max-low = 50-0 = %d\n", a[:], cap(a[:])) // slice: [0 1 2 3 4 5 6 7 8 9], cap: max-low = 50-0 = 50
fmt.Printf("slice: %v, cap: max-low = 50-3 = %d\n", a[3:5], cap(a[3:5])) // slice: [3 4], cap: max-low = 50-3 = 47
fmt.Printf("slice: %v, cap: max-low = 50-3 = %d\n", a[3:3], cap(a[3:3])) // slice: [], cap: max-low = 50-3 = 47
fmt.Printf("slice: %v, cap: max-low = 50-0 = %d\n", a[:5], cap(a[:5])) // slice: [0 1 2 3 4], cap: max-low = 50-0 = 50
fmt.Printf("slice: %v, cap: max-low = 50-4 = %d\n", a[4:], cap(a[4:])) // slice: [0 1 2 3 4], cap: max-low = 50-4 = 46
fmt.Printf("slice: %v, cap: max-low = 50-0 = %d\n", a[:50], cap(a[:50])) // slice: [0 1 2 3 4 5 6 7 8 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], cap: max-low = 50-0 = 50
}
スライス式で嵌ると少しつらい挙動
スライス式について簡単に説明してきましたが、スライス式のこのcapの確保によって意識しなければいけなくなる挙動があります。
まずは以下に例を示します。
package main
import "fmt"
func main() {
a := make([]int, 10, 50) // len: 10, cap: 50 のintのスライス
for i := 0; i < 10; i++ {
a[i] = i
}
b := a[3:6] // 3-5までちょっと取り出したくなった、中途半端
fmt.Printf("a: %v cap: %d, b: %v cap: %d\n", a, cap(a), b, cap(b)) // a: [0 1 2 3 4 5 6 7 8 9] cap: 50, b: [3 4 5] cap: 47 (1)
b[1] = 1000 // b[1]を1000にしたくなった
fmt.Printf("a: %v cap: %d, b: %v cap: %d\n", a, cap(a), b, cap(b)) // a: [0 1 2 3 1000 5 6 7 8 9] cap: 50, b: [3 1000 5] cap: 47 (2)
b = append(b, 100) // 気分で 100を最後尾に追加したくなった
fmt.Printf("a: %v cap: %d, b: %v cap: %d\n", a, cap(a), b, cap(b)) // a: [0 1 2 3 1000 5 100 7 8 9] cap: 50, b: [3 1000 5 100] cap: 47 (3)
}
(1)
に関しては、今まで説明してきた通り、3-5番目の要素の部分的取得なので [3 4 5]
のスライスが取得できるのはわかると思います。
(2)
に関しても、スライス式はあくまで元のスライスを部分的に取得したものなので b[1]
に代入すれば、それが a[5](=4)
に影響を与えることを知るのはそこまで難しくないと思います。
(3)
が少し厄介です。 スライス式で取り出した部分的なスライスは cap
も使い回します。そのため、 b
に新たな要素を append
すると a[7](=6)
の値に影響を与えます。 今回は自明なコードを書いていますが、cap
の存在は普段なかなか意識することがないので、例えば cap
を使い切ってGoがよしなに cap
を増やした時などに無意識にコードを書いしまって発生させると大分気づきにくいです。
package main
import "fmt"
func main() {
a := make([]int, 10)
for i := 0; i < len(a); i++ {
a[i] = i
}
fmt.Println(a, len(a), cap(a)) // [0 1 2 3 4 5 6 7 8 9] 10 10
// 何か事情があっていくつかappendする必要がある
a = append(a, 10)
a = append(a, 11)
// capがさらに確保されている。
fmt.Println(a, len(a), cap(a)) // [0 1 2 3 4 5 6 7 8 9 10 11] 12 20
b := a[4:10] // 4-9番目までとる
b = append(b, 2021) // 2021をappendする必要が出てきた
fmt.Println(a, b) // [0 1 2 3 4 5 6 7 8 9 2021 11] [4 5 6 7 8 9 2021]
}
ついでに回避方法も考える
ついでなので回避方法も色々考えてみます。
スライス式で取り出したスライスに変更を加えない
Read-Onlyなら特に意識する必要はないはずです。ただし言語の機能といった厳格なレベルで防げない気がします。lintで行けたりするのだろうか。
スライス式で取り出した後は大元のスライスは参照しない
大元のスライスが壊れても、それをどの機能も参照しないなら問題は発生しないでしょう。少し暗黙知が溜まったり、変更容易性が損なわれたりするかもしれません。
copyするようにする
スライス式で取り出した部分的なスライスに対して操作したい場合は、取り出した値をcopyすれば先ほどの挙動(2),(3)を回避できます。
package main
import "fmt"
func main() {
a := make([]int, 10, 50) // len: 10, cap: 50 のintのスライス
for i := 0; i < 10; i++ {
a[i] = i
}
b := make([]int, 6-3)
copy(b, a[3:6]) // 3-5までちょっと取り出したくなった、中途半端
fmt.Printf("a: %v, b: %v\n", a, b) // a: [0 1 2 3 4 5 6 7 8 9], b: [3 4 5] (1)
b[1] = 1000 // 最初の要素を1000にしたくなった
fmt.Printf("a: %v, b: %v\n", a, b) // a: [0 1 2 3 4 5 6 7 8 9], b: [3 1000 5] (2)
b = append(b, 100) // 気分で 100を最後尾に追加したくなった
fmt.Printf("a: %v, b: %v\n", a, b) // a: [0 1 2 3 4 5 6 7 8 9], b: [3 1000 5 100]
}
capを確保しないようにする
Full slice expressions
を使って cap
も指定することで回避できないか考えてみます。
package main
import "fmt"
func main() {
a := make([]int, 10, 50) // len: 10, cap: 50 のintのスライス
for i := 0; i < 10; i++ {
a[i] = i
}
b := a[3:6:6] // 3-5までちょっと取り出したくなった、中途半端
fmt.Printf("a: %v, b: %v\n", a, b) // a: [0 1 2 3 4 5 6 7 8 9], b: [3 4 5]
b[1] = 1000 // b[1]を1000にしたくなった
fmt.Printf("a: %v, b: %v\n", a, b) // a: [0 1 2 3 1000 5 6 7 8 9], b: [3 1000 5]
b = append(b, 100) // 気分で 100を最後尾に追加したくなった
fmt.Printf("a: %v, b: %v\n", a, b) // a: [0 1 2 3 1000 5 6 7 8 9], b: [3 1000 5 100]
}
cap
を確保していないので、(3)に関してはうまく回避することができました。ただしこの場合、(2)は回避できません。
おわりに
今回はGoのスライス式についてシンプルな例とともに簡単に記事に書かせていただきました。
このスライス式の挙動は順序立てて考えていけば普通の話に見えますが、うっかりだったりいざ直面すると気付きにくい部分もあるのかなと個人的には考えています。
なかなかプロダクトのコードで直面することがあるのか、と言われると自信がないですが、もし誰かの参考になれば幸いです。
基本的に仕様に関してはSpecのものを翻訳/解釈しているので、よければSpecを読んでみてください。