Go言語のキャッチアップ その3 ~スライス~

Goのスライス(Slice)

スライスは、「配列の一部または全部を参照する、可変長のデータ構造。」らしいが、よくわからん。 公式ドキュメント上では、

スライスは基底配列の連続した一部分を表し、その要素にインデックスを使ってアクセスできるもの として説明されている。 The Go Programming Language Specification

これもまだ抽象的に感じるので実際のコードを見る。

package main

import "fmt"

func main() {
    numbers := [5]int{10, 20, 30, 40, 50}

    s := numbers[1:4]

    fmt.Println(s)
    // [20 30 40]
}

なるほど。 numbers[1:4]と書くことで、配列のインデックス1から3までの要素を指定している。 つまり「配列のここからここまでをいまから使うぞ!」と指定しているようなもんか。 このnumbers[1:4]は、Goでは「スライス式(slice expression)」と呼ばれるらしい。 処理を整理すると、

  1. numbersという配列が存在する
  2. numbers[1:4]で、インデックス1~3の範囲を指定する。
  3. その範囲を扱うスライスが作成される
  4. 作られたスライスを変数sに代入する。

ここで、numbersとsは異なる型になっている。

fmt.Printf("%T\n", numbers)
// [5]int

fmt.Printf("%T\n", s)
// []int

numbersは配列、sはスライス。 ただし、スライスは単なる範囲指定の宣言ではなく、実際に作られる値である。 また、スライスは必ずしも既存の配列から作る必要はない。

s := []int{10, 20, 30}

このように書いた場合も、内部では要素を格納する配列が用意される。

スライスは元の配列をコピーしているわけではない。

ここで疑問が一つ浮かんだ。 numbers[1:4]でスライスを作った場合、元の配列からその範囲の値がコピーされているのだろうか? 結論コピーはされていない。

package main

import "fmt"

func main() {
    numbers := [5]int{10, 20, 30, 40, 50}

    s := numbers[1:4]

    s[0] = 999

    fmt.Println(s)
    // [999 30 40]

    fmt.Println(numbers)
    // [10 999 30 40 50]
}

このようにスライス側の値を変更すると、元の配列の値も変更されている。 関数の引数として配列を渡す場合、元の配列とは別の配列を関数側でコピーするが、スライスの場合元の配列を参照したままになるため、挙動が異なる。

配列の場合は、

a := [3]int{10, 20, 30}
b := a

b[0] = 999

fmt.Println(a) // [10 20 30]
fmt.Println(b) // [999 20 30]

このように代入時に配列全体がコピーされるため、bを変更してもaには影響しなかった。 しかしスライスは異なり、配列の要素をコピーするのではなく、元の配列のメモリ領域を参照している。 そのため、同じデータを操作することになる。

スライスと配列は相互に参照しているのか?

スライス側は元の配列を参照しているが、配列側はスライスを参照するのか? 結論NOで、参照しているのはスライスから配列の一方向のみ。

スライスの長さ(len)と容量(cap)について。

スライスには長さと容量という概念があり、それぞれlenとcapと呼ぶ。 調べてみると、

  • len: スライスからアクセスできる要素数
  • cap: スライスの先頭から基底配列の末尾までの容量

という説明が出てきた。「アクセスできる要素数」ってなんやねん。

もっと簡単に言えば「lenはスライスに含まれる要素の数」ということらしい。普通にlengthやな。

numbers := [5]int{10, 20, 30, 40, 50}

s := numbers[1:3]

fmt.Println(s)      // [20 30]
fmt.Println(len(s)) // 2

スライスsに含まれているのは20と30の2つだけ。なのでlen(s)は2となる。 ではcapは何なのか。

fmt.Println(cap(s)) // 4

これは4になる。なぜか? 元の配列は [10, 20, 30, 40, 50] で、スライスの開始位置はインデックス1, つまり20の位置。 そこから元の配列の末尾までを数えると、 20, 30, 40, 50の4つとなる。 だからcap(s)は4となる。

スライスの範囲を拡張する

numbers := []int{10, 20, 30, 40, 50}
s := numbers[1:4]
// [20 30 40]

fmt.Println(s[3])
// panic: runtime error: index out of range

現在のスライス s のインデックスは0〜2までなので、s[3] にアクセスするとpanicになる。 このように範囲外のエラーとなる。

スライスは容量の範囲内であれば、扱う範囲を拡張できる。

s = s[:4]

このようにすれば、スライスの先頭からindex4の手前まで、つまり4要素分を指定して新しいスライスを作ることができる。 スライスsを先頭から4要素分まで拡張するで、という感じ。

package main

import "fmt"

func main() {
    numbers := []int{10, 20, 30, 40, 50}

    s := numbers[1:4]

    fmt.Println(s)      // [20 30 40]
    fmt.Println(len(s)) // 3
    fmt.Println(cap(s)) // 4

    // スライスを4要素分に拡張
    s = s[:4]

    fmt.Println(s)      // [20 30 40 50]
    fmt.Println(len(s)) // 4
    fmt.Println(cap(s)) // 4

    fmt.Println(s[3])   // 50
}

まとめ

  • スライスは配列そのものではない。配列のどの範囲を扱うかという情報を持つもの。

  • スライスは元の配列の要素をコピーしない。基底配列のメモリ領域を参照しているため、スライス側で値を変更すると元の配列にも反映される。

  • 参照関係は一方向。スライスは基底配列を参照しているが、配列側はスライスの存在を知らない。

  • lenは現在スライスに含まれている要素数。

  • capはスライスの開始位置から、基底配列の末尾までの容量。

  • スライスはcapの範囲内であれば拡張できる。ただし、要素を新しく追加しているわけではなく、基底配列に存在する要素を扱える範囲を広げているだけ。