🎉

Goの配列とスライス入門

2022/10/24に公開

Goの学習をしていく中でスライスという概念がいまいちピンとこなかったので、自分なりにまとめます。

  • 配列とスライスってどういう関係なの
  • スライスの容量ってなに

と自分がわかりにくかったところを整理できればと思います。
細かい操作の方法などは省略します。

初学者による入門記事ですので間違い等あれば教えていただけると幸いです。

配列とスライス

スライスについて理解するにはスライスの背後に存在する配列について知る必要があります。
なのでA Tour of Goなどで学習しているといきなり配列の解説から入っていきますが、ここで他言語の配列をイメージしていると混乱しました。

Goにおいて、配列とスライスは以下のようなイメージを最初は持っておくとよいと思います。

  • 配列→固定長配列、あまり使われない
  • スライス→他言語の可変長配列のように扱えるもの、よく使う

配列

Goの配列とは、「同じ型のデータを集めて並べたデータ構造」です。

特徴

  • 要素の数があらかじめ決まっており変更できない
  • 要素の型は全て同じ
  • 要素数が違えば別の型

配列の初期化

Goの配列は以下のように初期化します。

// 要素数3の配列の初期化
array := [3]int{1, 2, 3}
fmt.Println(array)    //=> [1 2 3]
// 要素にアクセス
fmt.Println(array[1]) //=> 2

// 要素数を値から推測して初期化
array2 := [...]int{1, 2, 3}
fmt.Println(array2)    //=> [1 2 3]
fmt.Println(array2[1]) //=> 2

2つめの[...]を使った初期化はあくまでも値から要素の数を推測して初期化できるというだけで、あとから要素の数を変更することはできません。

array2 = [...]int{4, 5, 6}
fmt.Println(array2) // => [4 5 6]

// 初期化時と要素の数が違うのでエラーになる
array2 = [...]int{1, 2, 3, 4}
// => cannot use [...]int{…} (value of type [4]int) as type [3]int in assignment

スライス

A Tour fo Goによると、

スライスは配列への参照のようなものです。
スライスはどんなデータも格納しておらず、単に元の配列の部分列を指し示しています。
スライスの要素を変更すると、その元となる配列の対応する要素が変更されます。
同じ元となる配列を共有している他のスライスは、それらの変更が反映されます。

// スライスを作成
slice1 := []int{1, 2, 3, 4}

// 別のスライスに代入
slice2 := slice1

// スライス1の0番目の要素を変更
slice1[0] = 5

// 背後の共通の配列が変わるのでスライス2の要素も変わる
fmt.Println(slice1) // => [5 2 3 4]
fmt.Println(slice2) // => [5 2 3 4]

特徴

  • 他言語の可変長配列のように扱える
  • 背後に配列が存在する
  • 要素数は指定しなくて良い
  • 自動で配列も作られる

スライスの初期化

スライスの初期化の仕方も初見だと混乱しました。

// スライスの初期化
slice := []int{1, 2, 3}
fmt.Println(slice)    // => [1 2 3]
fmt.Println(slice[1]) // => 2

// make()を使って初期化
slice2 := make([]int, 3, 5)
fmt.Println(slice2) // => [0 0 0]
  • 配列の初期化と似ているが、要素の数を指定していない点([]の中が空)が異なる
  • make()を使っても初期化できる。

make()の第2引数と第3引数に渡しているのはスライスの長さと容量です。
ここでも容量ってなんやねんとなりました。

スライスの持つデータ

スライスは以下3つのデータを持ちます。

  • ポインタ
  • 長さ(length)
  • 容量(capacity)

ここではイメージがわきずらかった容量について解説します。

容量とは

容量とは、スライスが指しているポインタからスライスの背後にある配列の終わりまでの長さのことです。
スライスを後ろまでどれだけ拡張できるのかを表します。

以下のようなイメージです。

  • 長さはそのままスライスの長さ
  • 容量はスライスの始めから背後の配列の終わりまで

長さと容量はlen()cap()で取得できます。

 s := []int{1, 2, 3, 4, 5, 6}
fmt.Println(s) // => [1 2 3 4 5 6]
fmt.Println(len(s)) // => 6
fmt.Println(cap(s)) // => 6

// 容量は変わらない
s = s[:0]
fmt.Println(s) // => []
fmt.Println(len(s)) // => 0
fmt.Println(cap(s)) // => 6

// 容量は変わらない
s = s[:4]
fmt.Println(s) // => [1 2 3 4]
fmt.Println(len(s)) // => 4
fmt.Println(cap(s)) // => 6

// ポインタが変わるので容量が変わる
s = s[2:]
fmt.Println(s) // => [3 4]
fmt.Println(len(s)) // => 2
fmt.Println(cap(s)) // => 4

まとめ

  • 配列は固定長配列、スライスは可変長配列のように扱える
  • スライスはあくまでも配列を参照しているだけ
  • スライスは3つのデータ(ポインタ、長さ、容量)を持つ

Discussion