🔪

Goのスライスは配列の参照です

2024/09/11に公開

はじめに

実務でGoを使用することになったため勉強しはじめたところ、スライスにインデックス指定で取り出した値を代入した際に「ん?」と思ったところがあったため備忘のための記録です。

「ん?」となったところ

以下のような初期化したスライスにインデックス指定で再代入してみたときのこと。

Go Playground

s := []int{1, 2, 3, 4, 5, 6}
fmt.Println(s) // 出力: [1 2 3 4 5 6]
s = s[:0]
fmt.Println(s) // 出力: []
s = s[:4]      // <= なんで?エラーは?
fmt.Println(s) // 出力: [1 2 3 4] <= なんで?復活!!?!

ん?
空スライスに対して、Index4指定([:4])したらエラーにならないの?
最後の出力結果は[1 2 3 4]ではなく、[]じゃないの?

という状態に陥りました。パニック。

なんで

以下ををきちんと理解してなかったからでした。

  • スライスは配列の参照だということ。
  • スライスには長さとキャパシティという概念があること。

そもそもスライスとは

スライスは配列の参照です。
ドキュメントの冒頭にちゃんと書いてました。

Effective Go - The Go Programming Language

スライスは基礎となる配列への参照を保持し、あるスライスを別のスライスに代入すると、どちらも同じ配列を参照することになります。

スライスの定義

以下の場合、sはスライスとして定義されています。

s := []int{1, 2, 3, 4, 5, 6}

え?スライスは配列の参照なんだったら配列はどこに格納されてるの?って思いました。
どうやらGoランタイムによって動的配列としてヒープ領域に割り当てられてるみたいです。
なので、スライスはその配列に対する参照を保持しているということになります。

スライスが持つ情報

繰り返しですが、スライスは配列の参照です。
なので、スライス自体はデータをもってなく、以下の3つの情報を保持する構造体です。
https://github.com/golang/go/blob/4aa1efed4853ea067d665a952eee77c52faac774/src/runtime/slice.go#L15-L19

1. データを指すポインタ

元となる配列を指すポインタ。

2. スライスの長さ(len)

スライスが現在参照している要素の数のこと。
例えば、初期化時のスライス s := []int{1, 2, 3, 4, 5, 6} では、長さは6です。

スライスの持つ長さはlenで確認できます。

s := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("len=%d", len(s))
// len=6

3. スライスのキャパシティ(cap)

キャパシティは、スライスが参照している配列の最大の長さ、つまりスライスが伸ばせる限界のこと。

スライスの持つキャパシティはcapで確認できます。

s := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("cap=%d", len(s))
// cap=6

これらを踏まえて再確認

冒頭のコードのスライスの操作を順を追って確認する。

  1. スライスの初期化
    最初にスライスsが配列 [1, 2, 3, 4, 5, 6] を参照し、長さ6、キャパシティ6。
    s := []int{1, 2, 3, 4, 5, 6}
    fmt.Println(s)  // [1 2 3 4 5 6] len=6 cap=6
    
  2. スライスss[:0]を代入
    スライスsは空になったが、キャパシティは6のままで、元の配列への参照も残っている。
    s = s[:0]
    fmt.Println(s)  // [] len=0 cap=6
    
  3. スライスss[:4]を代入
    スライスsは長さ4まで伸ばされ、元の配列の最初の4つの要素を再び参照します。
    s = s[:4]
    fmt.Println(s)  // [1 2 3 4] len=4 cap=6
    

このように、s = s[:4]のような操作ができるのは、スライスがキャパシティの範囲内で再び元のデータを参照できるからです。

おわりに

スライスから一部を切り出して扱うようなケースでは簡単に事故れそうだなあと思ったので、気をつけたいと思います。

GitHubで編集を提案

Discussion