図解 Go Slice Tricks

図解 Go Slice Tricks

この記事はGo7 Advent Calendar 2019の記事です。

Go には Java や Ruby などのモダン言語のような slice を操作する関数は用意されていません。 Go では built-in 関数のappend()copy() と for 構文を組み合わせることで、他の言語にあるような slice 操作を実装できます。 この記事ではそれぞれの slice 操作を図解で解説します。

元ネタは Go 公式 Wiki にあるGo SliceTricks - golang/go Wikiです。

Slice の連結

sliceの連結 sliceの連結

a = append(a, b...)

コピー先 a にコピー元の b の全ての要素をコピーします。 連結後の長さが cap(a) を超える場合は、新たな slice が作成されて abともにコピーします。

以降はappend()のコピー先には十分なcapがあるという前提で進めます。

複製

sliceのコピー

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)

範囲の削除 (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)

単一要素の削除 (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)

順序保証しない単一要素の削除  (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)

フィルター(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 の挿入

sliceの挿入

a = append(a[:i], append(b, a[i:]...)...)

途中に slice を挿入します。 挿入する slice b の末尾に a[i:] を連結し、それを a[i] 以降にコピーします。

Push

Push

a = append(a, x)

末尾に要素 x を追加します。

Pop

Pop

x, a = a[len(a)-1], a[:len(a)-1]

末尾の要素を取り出し、長さを 1 つ切り詰めます。

Push Front/Unshift

Unshift

a = append([]T{x}, a...)

追加する要素を持つ長さ 1 の slice の末尾に a を連結したのを、新たな a とします。

Pop Front/Shift

Shift

x, a = a[0], a[1:]

先頭の要素を取り出し、a{1:] を新たな a とします。

おわりに

以上Go SliceTricks - golang/go Wikiの図解による解説でした。 Go には GC があるとはいえ、C と同じようにポインタを意識したほうが、よりよい Go のコードが記述できます。 そのとき、この slice 操作はコピーが発生するのか否かを考えられると、ハイパフォーマンスなプログラムが作れます。 解説した操作と図は、以下のサイトにチートシートとしてまとめてあります。


Profile picture

Shin'ya Ueoka

B2B向けSaaSを提供する会社の、元Webエンジニア。今はエンジニアリング組織のマネジメントをしている。