1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go】スライスにおけるappendのキャパシティ依存の挙動と解決策

Last updated at Posted at 2024-10-27

はじめに

  • Goのスライスは動的な配列として利用でき、要素数が固定されていないため、柔軟に扱えます
  • ただし、appendを使用して要素を追加する際には、スライスのキャパシティがどのように影響するかを理解しておく必要があります
  • 本記事では、appendの動作がスライスのキャパシティによってどのように変わるかを解説し、そのキャパシティ依存の問題を回避する方法の一つとしてフルスライス式を紹介します

本記事で話さないこと
本記事では挙動の違いについて理解することを重視するため、スライスの内部構造や、ポインタ、Goランタイムの正確な挙動については説明を省略します。

より詳細に知りたい方は、3. 関数と型 - 3.1. 型 - Gopher道場 -をご覧ください。スライスについては29:47〜、appendについては44:45〜から解説されています。

appendの基本的な動作

まず、appendの基本的な使い方を見てみましょう。

package main

import "fmt"

func main() {
	x := make([]int, 0, 5) // 長さ0、キャパシティ5のスライス
	x = append(x, 1, 2, 3, 4)
	y := x[:2]
	z := x[2:]

	fmt.Println(x, len(x), cap(x)) // 出力: [1 2 3 4] 4 5
	fmt.Println(y, len(y), cap(y)) // 出力: [1 2] 2 5
	fmt.Println(z, len(z), cap(z)) // 出力: [3 4] 2 3
}

このソースコードでは、長さ0、キャパシティ5のスライスxを作成し、appendで1から4までの値を追加します。その結果、xの内容は[1 2 3 4]となり、長さは4、キャパシティは5となります。

yzは、xのサブスライス(スライスのスライス)で、それぞれ[1 2][3 4]を指しています。

余談
yzのキャパシティの値が異なる点について簡単に補足します。スライス後のキャパシティはスライス元のキャパシティ - スライスの開始位置となります。そのため、yのキャパシティは5 - 0 = 5zのキャパシティは5 - 2 = 3となります。

キャパシティが足りない場合の挙動

では、キャパシティが足りない場合の挙動から見てみましょう。

package main

import "fmt"

func main() {
	x := make([]int, 0, 5) // 長さ0、キャパシティ5のスライス
	x = append(x, 1, 2, 3, 4)
	y := x[:2]
	z := x[2:]

	y = append(y, 30, 40, 50, 60)

	fmt.Println(x, len(x), cap(x)) // 出力: [1 2 3 4] 4 5
	fmt.Println(y, len(y), cap(y)) // 出力: [1 2 30 40 50 60] 6 10
	fmt.Println(z, len(z), cap(z)) // 出力: [3 4] 2 3
}

このコードでは、yに30、40、50、60を追加しますが、60を追加する際にキャパシティが不足します。そのため、appendは新しいスライスを生成し、その際にキャパシティが倍増して10になります(必ず倍増するわけではありませんが)。結果として、yの内容は[1 2 30 40 50 60]となります。

注目したいのは、この場合、yの要素の変更がxzに影響しないということです。

キャパシティが足りている場合の挙動

次に、キャパシティが足りている場合のappendの挙動を見てみましょう。

package main

import "fmt"

func main() {
	x := make([]int, 0, 5) // 長さ0、キャパシティ5のスライス
	x = append(x, 1, 2, 3, 4)
	y := x[:2]
	z := x[2:]

    // fmt.Println(x, len(x), cap(x)) // 出力: [1 2 3 4] 4 5
	// fmt.Println(y, len(y), cap(y)) // 出力: [1 2] 2 5
	// fmt.Println(z, len(z), cap(z)) // 出力: [3 4] 2 3

	y = append(y, 30, 40, 50)

	fmt.Println(x, len(x), cap(x)) // 出力: [1 2 30 40] 4 5
	fmt.Println(y, len(y), cap(y)) // 出力: [1 2 30 40 50] 5 5
	fmt.Println(z, len(z), cap(z)) // 出力: [30 40] 2 3
}

このコードでは、yに30、40、50を追加しますが、yの元のキャパシティは5のままで、appendした結果も問題なく保存されています。一方で、xのスライスの内容は[1 2 30 40]zのスライスの内容は[30 40]と、影響を受けていることが分かるかと思います。

ここでの注目点は2点あります。

  • yでの要素の変更が、xzにも影響を与える
  • yに要素を追加しても、xzに影響はない
    • xzについて、30 40と要素が変更されている一方で、60の追加はされていません

解説:なぜ挙動に差が出るのか?

どうしてキャパシティ依存でappendの挙動に差が出るのか、改めて説明しようと思います。

キャパシティが足りない場合のappendの動作

キャパシティが足りない場合、Goランタイムは追加のメモリを確保して新しい配列を作成し、元のデータをそこにコピーしてから新しい要素を追加します。
そのため、append の結果として生成される新しいスライスは、元のスライス(x)とは異なる配列を参照するようになり、他のスライスには影響を与えません。

キャパシティが足りている場合のappendの動作

スライスのキャパシティが足りている場合、appendを使っても新たにメモリは確保されません(これはメモリ管理を効率的に行うためです)。
そのため、新しい配列が作成されることなく、既存の配列に要素が追加されます。これにより、元のスライス(x)とそのサブスライス(yz)は同じ配列を参照し続けるため、yzの要素を変更すると、xにもその変影響を与えることになります。

このように、キャパシティが足りているかどうかでappendの動作が異なるため、挙動に差が生まれます。

append時のキャパシティ依存問題の解決策:フルスライス式

appendのキャパシティ依存問題を回避する方法として、フルスライス式を紹介します。次のソースコードを見てください。

package main

import "fmt"

func main() {
	x := make([]int, 0, 5) // 長さ0、キャパシティ5のスライス
	x = append(x, 1, 2, 3, 4)
	y := x[:2:2]  // フルスライス式を使用
	z := x[2:4:4] // フルスライス式を使用

    // fmt.Println(x, len(x), cap(x)) // 出力: [1 2 3 4] 4 5
	// fmt.Println(y, len(y), cap(y)) // 出力: [1 2] 2 2
	// fmt.Println(z, len(z), cap(z)) // 出力: [3 4] 2 2
 
	y = append(y, 30, 40, 50)

	fmt.Println(x, len(x), cap(x)) // 出力: [1 2 3 4] 4 5
	fmt.Println(y, len(y), cap(y)) // 出力: [1 2 30 40 50] 5 6
	fmt.Println(z, len(z), cap(z)) // 出力: [3 4] 2 2
}

このソースコードでは、yzをフルスライス式で作成しました。
フルスライス式は次のように記述します。

// low  = スライスの開始位置
// high = スライスの終了位置(このインデックスの手前までが含まれる)
// max  = スライス元の参照上限位置(キャパシティは「max- low」で決まる)
slice[low : high : max]
  • y := x[:2:2]
    • x[:2]で、xの先頭から2番目までの要素を参照
    • 最後の :2 は、スライス元(x)の参照上限位置を2に制限
    • yのキャパシティは2 - 0 = 2
  • z := x[2:4:4]
    • x[2:4] で、xの3番目と4番目の要素を参照
    • 最後の :4は、スライス元(x)の参照上限位置を4に制限
    • zのキャバシティは4 - 2 = 2

フルスライス式ではキャパシティを明示的に制限することで、yへの要素追加がxzには影響を与えません。(※要素の変更による影響は依然と残ります)

まとめ

  • Goのスライスにおけるappendの基本的な動作や、キャパシティによる挙動の違いについて説明しました
  • キャパシティ不足時は新しいスライスが作成され、キャパシティが十分な場合は同じ配列が参照され続けます
  • フルスライス式を使用することで、キャパシティ依存による影響を最小限に抑え、append時の上書きを避けることが可能です

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?