この記事はGo7 Advent Calendar 2019の記事です。
Go には Java や Ruby などのモダン言語のような slice を操作する関数は用意されていません。
Go では built-in 関数のappend()
やcopy()
と for 構文を組み合わせることで、他の言語にあるような slice 操作を実装できます。
この記事ではそれぞれの slice 操作を図解で解説します。
元ネタは Go 公式 Wiki にあるGo SliceTricks - golang/go Wikiです。
Slice の連結
a = append(a, b...)
コピー先 a
にコピー元の b
の全ての要素をコピーします。
連結後の長さが cap(a)
を超える場合は、新たな slice が作成されて a
、b
ともにコピーします。
以降はappend()
のコピー先には十分なcap
があるという前提で進めます。
複製
slice a
から新たな slice b
を複製します。
以下のいずれかの方法で複製できます。
b = make([]T, len(a))
copy(b, a)
b = append([]T(nil), a...)
コピー元 a
が空か nil かを区別する場合は、以下のように書きます。
a
が nil の場合はb
が nil になり、a
が空の場合はb
が空になります。
b = append(a[:0:0], a...)
範囲の削除
a = append(a[:i], a[j:]...)
範囲を指定して削除します。
a[:i]
までを切り出して、a[j:]
の各要素を a[i]
以降にコピーします。
単一要素の削除
a = append(a[:i], a[i+1:]...)
a = a[:i+copy(a[i:], a[i+1:])]
a[:i]
を切り出して、a[i+1:]
の各要素を a[i]
以降にコピーします。
順序保証しない単一要素の削除
a[i] = a[len(a)-1]
a = a[:len(a)-1]
要素の順序が重要じゃない場合は、末尾の要素を削除対象の要素にコピーできます。 この方法は「単一要素の削除」と比較して、コピーが 1 回で済みます。
範囲の削除 (GC)
copy(a[i:], a[j:])
for k, n := len(a)-j+i, len(a); k < n; k++ {
a[k] = nil // or the zero value of T
}
a = a[:len(a)-j+i]
要素がポインタ、またはポインタを持つ struct の場合、上記の「範囲の削除」でメモリリークが発生します。 なぜならコピー前に末尾にあった要素がポインタへの参照を持ち続け、garbage collection (GC) が働かないためです。 これを回避するために、要素のコピー後に使わなくなった要素に nil またはゼロ値で参照を消します。
単一要素の削除 (GC)
if i < len(a)-1 {
copy(a[i:], a[i+1:])
}
a[len(a)-1] = nil // or the zero value of T
a = a[:len(a)-1]
「単一要素の削除」も同様に、使わなくなった要素を nil またはゼロ値で参照を消します。
順序保証しない単一要素の削除 (GC)
a[i] = a[len(a)-1]
a[len(a)-1] = nil
a = a[:len(a)-1]
「順序保証しない単一要素の削除」も同様に、使わなくなった要素を nil またはゼロ値で参照を消します。
途中に要素を追加
a = append(a[:i], append(make([]T, j), a[i:]...)...)
途中に要素を追加して要素を拡張します。
新たに長さ j
の slice を作成し、その末尾にa[i:]
を追加します。
その結果を a[i]
以降にコピーします。
末尾に要素を追加
a = append(a, make([]T, j)...)
末尾に要素を追加して要素を拡張します。
新たに長さ j
の slice を作成し a
の末尾に追加します。
フィルター(in place)
n := 0
for _, x := range a {
if keep(x) {
a[n] = x
n++
}
}
a = a[:n]
条件のマッチする要素のみを残します。 新たな slice を作るのではなく、マッチする要素を sliec の先頭から詰めて、最後に長さを切り詰めます。
要素の挿入
a = append(a[:i], append([]T{x}, a[i:]...)...)
途中に要素を挿入します。
要素x
のみを含む長さ 1 の slice の末尾に a[i:]
を連結し、それをa[i]
以降にコピーします。
slice の挿入
a = append(a[:i], append(b, a[i:]...)...)
途中に slice を挿入します。
挿入する slice b
の末尾に a[i:]
を連結し、それを a[i]
以降にコピーします。
Push
a = append(a, x)
末尾に要素 x
を追加します。
Pop
x, a = a[len(a)-1], a[:len(a)-1]
末尾の要素を取り出し、長さを 1 つ切り詰めます。
Push Front/Unshift
a = append([]T{x}, a...)
追加する要素を持つ長さ 1 の slice の末尾に a
を連結したのを、新たな a
とします。
Pop Front/Shift
x, a = a[0], a[1:]
先頭の要素を取り出し、a{1:]
を新たな a
とします。
おわりに
以上Go SliceTricks - golang/go Wikiの図解による解説でした。 Go には GC があるとはいえ、C と同じようにポインタを意識したほうが、よりよい Go のコードが記述できます。 そのとき、この slice 操作はコピーが発生するのか否かを考えられると、ハイパフォーマンスなプログラムが作れます。 解説した操作と図は、以下のサイトにチートシートとしてまとめてあります。