📖

Golangにおける配列とスライス

2023/05/11に公開

概要

Golangの配列とスライスについてまとめてみました。

配列

指定した型の要素をメモリ上に連続して保持するデータ構造。
初期化時に要素数を指定する必要があるので、指定した数以上を保有する事はできない(ので少し不便)。
イメージ図

使い方

func main() {
	var arr [4]int
	fmt.Println(arr) // => [0 0 0 0]

	arr[0] = 1
	fmt.Println(arr) // => [1 0 0 0]

	arr[5] = 5 // => invalid argument: index 5 out of bounds [0:4]
}

同じ要素をまとめて管理できるのは便利だが固定長なため柔軟性に欠けるところもあります。

スライス

スライスとは

指定した型の要素を複数保有できるデータ構造。
上述した配列と違い保有できる要素数に上限もなく好きなだけ追加することが出来ます。

使い方

func main() {
	s := make([]int, 0, 4)
	fmt.Println(s) // => []
	fmt.Println(len(s)) // => 0
	for i := 0; i < 4; i++ {
		s = append(s, i)
	}
	fmt.Println(s) // => [0 1 2 3]
	fmt.Println(len(s)) // => 4
}

配列と違い上限がないので好きなだけ要素を追加することが可能です。

スライスの実態

golangにおいてスライスは上述した配列の先頭のアドレスを保持している構造体として定義されています。
https://github.com/golang/go/blob/master/src/runtime/slice.go#L15-L19

イメージ

ptr が配列の先頭を指し示しています。

そしてlen は現在の要素数、cap は最大要素数です。
なので必ず len <= cap が成り立ちます。
cap以上の要素を持てないなら配列と変わらないのではないか??と最初は思いましたが、上限に達したタイミングで最大要素数が増えるようになっているみたいです。

実験

初期化時長さが0のスライスに値を追加していって最大要素数がどのように増えていっているか調べます。

func main() {
	s := make([]int, 0)
	printSliceInfo(s) // => slice: [], len: 0, cap: 0

	s = append(s, 1)
	printSliceInfo(s) // => slice: [1], len: 1, cap: 1

	s = append(s, 2)
	printSliceInfo(s) // => slice: [1 2], len: 2, cap: 2

	s = append(s, 3)
	printSliceInfo(s) // => slice: [1 2 3], len: 3, cap: 4

	s = append(s, 4)
	printSliceInfo(s) // => slice: [1 2 3 4], len: 4, cap: 4

	s = append(s, 5)
	printSliceInfo(s) // => slice: [1 2 3 4 5], len: 5, cap: 8
}

func printSliceInfo(s []int) {
	fmt.Printf("slice: %v, len: %d, cap: %d\n", s, len(s), cap(s))
}

要素を上限いっぱいまで保持している状態から新しく要素を追加しようとすると最大要素数が2倍に増えていることが分かりました。

またコードレベルでも確認してみました。
https://github.com/golang/go/blob/master/src/runtime/slice.go#L157-L284
元々の上限が256未満の場合はやはり2倍にするように書かれています。
また memmove 関数を使って新しく確保したメモリ領域に元々の配列の内容をコピー?移動?して配列を作り直しているみたいです。

役に立つかもしれないtips

配列とスライスの構造については理解できたので配列・スライスを扱っていく上で役に立ちそうなtipsを書き連ねてみました。

配列において要素数も型の一部

下記のように同じint型でも要素数が違う配列は別の型とみなされるので注意が必要かも。

func main() {
	arr := [5]int{}
	arr = [10]int{} // => cannot use [10]int{} (value of type [10]int) as [5]int value in assignment
}

nilでも操作可能

下記のようにスライスを初期化せず宣言しているだけの場合でも append len などの組み込み関数を使ってもPanicにならない。

func main() {
	var s []int
	fmt.Println(s == nil) // => true
	s = append(s, 1)
	fmt.Println(s) // => [1]
}

なのでappendする前にnilチェックをするなどは不要みたい。

コピーするときはコピー先の長さを指定しておく

以下のようにコピー先の長さを予め指定していない場合スライスのコピーはうまくいかない。

func main() {
	src := []int{1, 2, 3, 4, 5}
	var dst []int
	copy(dst, src)
	fmt.Println(dst) // => [] 空っぽ
}

これは copy 関数がコピー元・先の小さい方の長さ分しかコピーを実行しないためです。
https://github.com/golang/go/blob/master/src/builtin/builtin.go#L149-L154

なので解決策としてはコピー先を指定する際に長さを指定してあげれば良いと思います。

func main() {
	src := []int{1, 2, 3, 4, 5}
	dst := make([]int, len(src)) // srcの全要素コピーしたいので長さも合わせる
	copy(dst, src)
	fmt.Println(dst)
}

参考

https://go.dev/blog/slices-intro
100 Go Mistakes

Discussion