Simple minds think alike

より多くの可能性を

【Golang】スライスと配列の特徴、違いについて

スライスはGo言語の中でもっとも利用されるデータ構造だと思いますが、挙動が複雑で仕様を把握するのが難しく、利用頻度が高いがゆえによく分からず使っているとパフォーマンスの低下を招き易い機能と言えるかと思います。

スライス及び配列の特徴を把握することで、より実行効率の高いGoのコードを書けるようになるようになります。より詳細な情報を知りたい方は、以下の公式ブログのドキュメントを読まれることをお勧めします。

配列とスライスの特徴、違い

配列もスライスも、以下のような特定のデータ型(String等)の値を一定の長さの分だけ格納できるデータの入れ物のようなものができる点は同じです。

それぞれの特徴としては、主に

  • ①長さが固定か可変か
  • ②参照型かどうか

2つのが挙げられます。

①長さが固定か可変か

一番大きな違いとしては、最初に変数を定義した後、要素を増やしたりできるかどうかという点です。

  • 配列(固定長配列)
  • スライス(可変長配列)

以下で例を示しますが、配列・スライスのそれぞれの定義の仕方の相違点は

  • 配列: 長さの部分が固定または省略表記(etc. [3], [...])
  • スライス: 長さの部分が未指定、またはmakeで生成

という部分です。

【配列の場合】

//  長さの部分を3に固定して配列を定義
people := [3]string{
  "竈門 炭治郎(かまど たんじろう)", 
  "我妻 善逸(あがつま ぜんいつ)", 
  "嘴平 伊之助(はしびら いのすけ)",
}
// 定義後長さを変えられず、要素を増やせない。appendできない。
// [エラー] => people = append(people, "竈門 禰豆子(かまど ねずこ)")

//  省略表記で配列を定義
people2 := [...]string{
  "竈門 炭治郎(かまど たんじろう)", 
  "我妻 善逸(あがつま ぜんいつ)", 
  "嘴平 伊之助(はしびら いのすけ)",
}

// peopleとpeople2は内容が同じ配列
reflect.DeepEqual(people, people2)
=> true

【スライスの場合】

//  長さの部分を未指定にしてスライスを定義
people := []string{
  "竈門 炭治郎(かまど たんじろう)", 
  "我妻 善逸(あがつま ぜんいつ)", 
  "嘴平 伊之助(はしびら いのすけ)",
}
// 定義した後追加できる
people = append(people, "竈門 禰豆子(かまど ねずこ)")

//  makeを使って長さが4のスライスを定義
people2 := make([]string, 4)
people2[0] = "竈門 炭治郎(かまど たんじろう)"
people2[1] = "我妻 善逸(あがつま ぜんいつ)"
people2[2] = "嘴平 伊之助(はしびら いのすけ)"
people2[3] = "竈門 禰豆子(かまど ねずこ)"

// peopleとpeople2は内容が同じスライス
reflect.DeepEqual(people, people2)
=> true

②参照型かどうか

2つ目の違いは参照型かどうかです。配列とスライスは、定義の仕方がちょっと違うだけですが、関数に渡した時や変数に代入した時の挙動が異なります。

スライスは参照型であり、例えば関数の引数として指定した場合、参照が渡るため関数の中で値が変更されていれば、関数の外でも変更が反映されます。

配列の場合は、値渡しになるため関数内での変更が、関数の外に反映されません。(参照渡し・値渡しの違いに関しては別の記事で記載しています。)

【配列(値渡し)の場合】

func ChangeToNezuko(people [3]string) {
  people[0] = "竈門 禰豆子(かまど ねずこ)"
}

peopleArray := [3]string{
  "竈門 炭治郎(かまど たんじろう)", 
  "我妻 善逸(あがつま ぜんいつ)", 
  "嘴平 伊之助(はしびら いのすけ)",
}

// 関数の中で、配列の1つ目の要素"竈門 炭治郎(かまど たんじろう)"を
// "竈門 禰豆子(かまど ねずこ)"に変えようとしてみる
ChangeToNezuko(peopleArray)

//  関数の外には変更が反映されない
fmt.Println(peopleArray[0])
=>  "竈門 炭治郎(かまど たんじろう)"

【スライス(参照渡し)の場合】

func ChangeToNezuko(people []string) {
  people[0] = "竈門 禰豆子(かまど ねずこ)"
}

peopleSlice := []string{
  "竈門 炭治郎(かまど たんじろう)", 
  "我妻 善逸(あがつま ぜんいつ)", 
  "嘴平 伊之助(はしびら いのすけ)",
}

// 関数の中で、スライスの1つ目の要素"竈門 炭治郎(かまど たんじろう)"を
// "竈門 禰豆子(かまど ねずこ)"に変えてみる
ChangeToNezuko(peopleSlice)

//  関数の外でも変更が反映されている
fmt.Println(peopleSlice[0])
=>  "竈門 禰豆子(かまど ねずこ)"

なお、配列の場合でも、関数への渡し方を変えて、配列のポインタを関数の引数にすることで、スライス同様に関数内の変更を外に反映させることができます。

【配列(参照渡し)の場合】

func ChangeToNezuko(people *[3]string) {
  people[0] = "竈門 禰豆子(かまど ねずこ)"
}

peopleArray := [3]string{
  "竈門 炭治郎(かまど たんじろう)", 
  "我妻 善逸(あがつま ぜんいつ)", 
  "嘴平 伊之助(はしびら いのすけ)",
}

// 関数の中で、配列の1つ目の要素"竈門 炭治郎(かまど たんじろう)"を
// "竈門 禰豆子(かまど ねずこ)"に変えてみる
ChangeToNezuko(&peopleArray)

//  関数の外でも変更が反映されている
fmt.Println(peopleArray[0])
=>  "竈門 禰豆子(かまど ねずこ)"

スライスと配列の構造的な相違

スライスの構造は reflect.SliceHeader で確認することができるのですが、配列のラッパーになっていて以下の構造になっています。

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

それぞれ

  • Dataには、配列のポインタが格納されて
  • Lenには、長さ
  • Capには、容量

が保存されています。

図にすると以下のようになります。

コード上でLen(長さ)、Cap(容量)を確認する際は、 len()cap() の引数にスライスを指定します。

people := []string{
  "竈門 炭治郎(かまど たんじろう)", 
  "我妻 善逸(あがつま ぜんいつ)", 
  "嘴平 伊之助(はしびら いのすけ)",
}

len(people)
=> 3
cap(people)
=> 3

長さを未指定にすることで、長さと容量が同じスライスを生成することができますが、 make を使うことで長さと容量が異なるスライスを作ることができます。

メモリ領域の確保は比較的重い処理なので、定義の時点で必要な領域(容量)を事前に確保しておくことで、パフォーマンスの高いプログラムを実装できます。

make の1番目の引数でデータ型、2番目の引数でスライスの長さ、3番目の引数で容量を指定します。

people := make([]string, 3, 4)
people[0] = "竈門 炭治郎(かまど たんじろう)"
people[1] = "我妻 善逸(あがつま ぜんいつ)"
people[2] = "嘴平 伊之助(はしびら いのすけ)"

fmt.Println(people)
=> [竈門 炭治郎(かまど たんじろう) 
    我妻 善逸(あがつま ぜんいつ) 
    嘴平 伊之助(はしびら いのすけ)]

// スライス定義時と同じ長さ、容量
len(people)
=> 3
cap(people)
=> 4

// 長さ3なので、4番目の要素に値を入れようとするとエラーになる
// people[3] = "竈門 禰豆子(かまど ねずこ)"
// => panic: runtime error: index out of range [3] with length 3

// appendで要素を追加できる
people = append(people, "竈門 禰豆子(かまど ねずこ)")
[]string{
  "竈門 炭治郎(かまど たんじろう)",
  "我妻 善逸(あがつま ぜんいつ)",
  "嘴平 伊之助(はしびら いのすけ)",
  "竈門 禰豆子(かまど ねずこ)",
}

// 要素を追加したことで長さが4、容量が4になる
len(people)
=> 4
cap(people)
=> 4

追記

スライスの様々な操作 ( 簡易スライス式 / copy / append ) の挙動に関しては他の記事にまとめてみました。もし、よろしければこちらもご参照ください。 simple-minds-think-alike.hatenablog.com

参考資料