Go言語のキャッチアップ その4 ~スライスのmakeとappend~

make関数

スライスを作成するときに、長さ(len), 容量(cap)を指定できる関数

numbers := make([]int, 3, 5)

これは、

  • []int: 整数を扱うスライスを作成する
  • 3: 長さ(len)は3
  • 5: 容量(cap)は5

という意味。 容量の入力を省略することができる。

numbers := make([]int, 3)
// make関数を使ったスライスの作成
package main

import "fmt"

func main() {
	numbers := make([]int, 3)

	fmt.Println(numbers)      // [0 0 0]
	fmt.Println(len(numbers)) // 3
	fmt.Println(cap(numbers)) // 3
}

具体的な要素の値はまだ決定していないが、長さ(len)や容量(cap)を指定してスライスを作成したいときに使える。

上記の通り、makeで作成した要素にはゼロ値が入る。

numbers := make([]int, 3)
fmt.Println(numbers) // [0 0 0]

なぜ[0 0 0]になるのか。 これは、Goではmakeで作成したスライスの要素が、その型のゼロ値で初期化されるため。

numbers := make([]int, 3)
// [0 0 0]

names := make([]string, 3)
// ["" "" ""]

flags := make([]bool, 3)
// [false false false]

append関数

appendは、スライスの末尾に要素を追加するための関数。

package main

import "fmt"

func main() {
	numbers := []int{10, 20, 30}
	numbers = append(numbers, 40)
	fmt.Println(numbers)
	// [10 20 30 40]
}
  • append(numbers, 40): numbersの末尾に40を追加したスライスを返す

つまり、appendは元のスライス変数を直接書き換えるのではなく、要素を追加した結果のスライスを返す。

makeとappendを組み合わせると?

a := make([]int, 3)
a = append(a, 10)

fmt.Println(a)
// [0 0 0 10]

makeでスライスを作成した時点で、すでに3つの要素が存在する。したがって、appendすると4番目の要素として10が追加される。 一方で、

b := make([]int, 0, 3)
b = append(b, 10)

fmt.Println(b)
// [10]

こちらは長さが0なので、10が最初の要素になる。

appendでcap(容量)を超えるとどうなるか?

package main

import "fmt"

func main() {
    numbers := make([]int, 0, 3)

    fmt.Println(len(numbers)) // 0
    fmt.Println(cap(numbers)) // 3

    numbers = append(numbers, 10)
    numbers = append(numbers, 20)
    numbers = append(numbers, 30)

    fmt.Println(numbers)      // [10 20 30]
    fmt.Println(len(numbers)) // 3
    fmt.Println(cap(numbers)) // 3
}

このように長さ0, 容量3のスライスを冒頭で作成したあとに、10, 20, 30をappendしていく。 最後のPrintlnでは、長さ3, 容量3となっていることがわかる。 ここで4つ目の要素をappendしたらどうなるか?

numbers = append(numbers, 40)

fmt.Println(numbers)      // [10 20 30 40]
fmt.Println(len(numbers)) // 4
fmt.Println(cap(numbers)) // 4以上(実装による)

このようにappendではcap(容量)を超えてもpanicにならない。 容量が足りなくなると、Goが自動でより大きな基底配列を用意してくれるため。

処理の流れは以下のようになっている。

  1. 現在の基底配列には3要素分の容量しかない
  2. append(numbers, 40)で4つ目を追加しようとする
  3. 容量が足りないので、Goが新しい基底配列を確保する
  4. 元の要素10,20,30を新しい配列にコピーする
  5. 40を追加したスライスを返す。

なぜcapは4ではなく6になるのか?

要素を一つ追加するたびに毎回新しい配列を作っていると、コピー処理が何度も発生してしまう。 そのため、Goは必要な容量より多めにメモリを確保することがある。 ただし、容量が何倍に増えるかはGoの言語仕様で保証されていないらしい。 capが必ず6になるわけではない。

範囲外の容量でスライス拡張したときはpanicになっていなかったか?

s = s[:5]

範囲外のスライス拡張をした場合には、panicが発生する。 一方appendは容量が足りなければ新しい基底配列を確保できる。