14. スライスの内部構造と要素の追加
- 14.1 スライスの内部構造
- 14.2 スライスへの要素追加とキャパシティ
ポイントは次の3つです。
- スライスは配列へのポインタ、長さ、キャパシティしかもたない
- キャパシティを超えずにスライスに要素を追加すると、スライスが参照している配列の値も変更する
- キャパシティを超えてスライスに要素を追加すると、スライスはもともと参照していた配列またはスライスとは別に、新たな配列をメモリ上に作成してそれを参照する
14.1 スライスの内部構造
配列のメモリの使い方
スライスの内部構造を知るために、まずは配列のメモリの使い方について説明します。
次の配列を例に、配列のメモリの使い方を見ていきます。
// 配列aを定義 a := [4]int{1, 2, 3, 4} fmt.Println(a) // "[1 2 3 4]"が出力される fmt.Printf("%p\n", &a[0]) // "0x1040e130"が出力される fmt.Printf("%p\n", &a[1]) // "0x1040e134"が出力される fmt.Printf("%p\n", &a[2]) // "0x1040e138"が出力される fmt.Printf("%p\n", &a[3]) // "0x1040e13c"が出力される
このときの配列は次の図のような状態です。
メモリには配列の各要素の領域が確保され、それぞれにアドレスが割り振られています。
スライスのメモリの使い方
続けてスライスのメモリの使い方を見ていきます。
先ほどの配列の例に続けてにスライスを作成します。
// 配列aを定義 a := [4]int{1, 2, 3, 4} fmt.Println(a) // "[1 2 3 4]"が出力される fmt.Printf("%p\n", &a[0]) // "0x1040e130"が出力される fmt.Printf("%p\n", &a[1]) // "0x1040e134"が出力される fmt.Printf("%p\n", &a[2]) // "0x1040e138"が出力される fmt.Printf("%p\n", &a[3]) // "0x1040e13c"が出力される // aの配列のインデックス1~3を指定 s := a[1:] fmt.Println(s) // "[2 3 4]"が出力される fmt.Printf("%p\n", &s[0]) // "0x1040e134"が出力される fmt.Printf("%p\n", &s[1]) // "0x1040e138"が出力される fmt.Printf("%p\n", &s[2]) // "0x1040e13c"が出力される
スライスをPrintlnで出力すると、[2, 3, 4] の配列を返しますが、
各要素のアドレスを出力すると、配列aのアドレスが返ってきます。
このときのスライスは次の図のような状態です。
スライスは、内部構造に配列へのポインタ、要素の長さ、キャパシティしか持たず、配列の値は持っていません。
スライスが配列を参照する方法
では、スライスはどのようにして配列を持っているようなふるまいをしているのか見てみます。
スライスは参照先の配列の、最初のアドレスを持っているのでそのアドレスの値を参照します。
次に、長さ「3」なので、参照先の値から3つ値を参照します。
ちなみに、スライスを初期化した場合は、スライスは新たに配列をメモリ上に作成し、それを参照します。
14.2 スライスへの要素追加とキャパシティ
スライスに要素を追加するには組み込み関数 append を使います。
append(追加先のスライス, 追加する要素1, 追加する要素2, ・・・) // ここでの"・・・"は可変長引数の意味で使っています。
append の挙動はスライスのキャパシティを超えるか超えないかによって違います。
- キャパシティを超えずにスライスに要素を追加する場合
- キャパシティを超えてにスライスに要素を追加する場合
キャパシティを超えずに要素を追加する場合
// 長さ1、キャパシティ2のスライスを作成 s := make([]int, 1, 2) fmt.Println(s) // "[0]"が表示される fmt.Printf("%p\n", &s[0]) // "0x1040a128"が出力される fmt.Printf("長さ:%d\n", len(s)) // "長さ:1"が出力される fmt.Printf("キャパシティ:%d\n", cap(s)) // "キャパシティ:2"が出力される
このとき、スライス s は次の図のような状態です。
次に、スライス s に要素を1つ追加します。
s := make([]int, 1, 2) fmt.Println(s) // "[0]"が表示される fmt.Printf("%p\n", &s[0]) // "0x1040a128"が出力される fmt.Printf("長さ:%d\n", len(s)) // "長さ:1"が出力される fmt.Printf("キャパシティ:%d\n", cap(s)) // "キャパシティ:2"が出力される // 要素を1つ追加 fmt.Println("--要素を追加--") s = append(s, 10) fmt.Printf("%p\n", &s[0]) // "0x1040a128"が出力される fmt.Printf("長さ:%d\n", len(s)) // "長さ:2"が出力される fmt.Printf("キャパシティ:%d\n", cap(s)) // "キャパシティ:2"が出力される
このとき、スライス s は次の図のような状態です。
appendによってキャパシティを超えずに要素を追加する場合、
- スライスが参照している配列の値を追加するのではなく、変更する
という振る舞いをします。
キャパシティを超えて要素を追加する場合
先ほどのスライス s に、さらに要素を1つ追加します。
s := make([]int, 1, 2) fmt.Println(s) // "[0]"が表示される fmt.Printf("%p\n", &s[0]) // "0x1040a128"が出力される fmt.Printf("長さ:%d\n", len(s)) // "長さ:1"が出力される fmt.Printf("キャパシティ:%d\n", cap(s)) // "キャパシティ:2"が出力される // 要素を1つ追加 fmt.Println("--要素を追加--") s = append(s, 10) fmt.Printf("%p\n", &s[0]) // "0x1040a128"が出力される fmt.Printf("長さ:%d\n", len(s)) // "長さ:2"が出力される fmt.Printf("キャパシティ:%d\n", cap(s)) // "キャパシティ:2"が出力される // 要素を1つ追加 fmt.Println("--要素を追加--") s = append(s, 10) fmt.Printf("%p\n", &s[0]) // "0x1040a190"が出力される fmt.Printf("長さ:%d\n", len(s)) // "長さ:3"が出力される fmt.Printf("キャパシティ:%d\n", cap(s)) // "キャパシティ:4"が出力される
スライス s のキャパシティが元の2倍(2 → 4)になり、s[0]
のアドレスが変更されました。
このとき、スライス s は次の図のような状態です。
appendによってキャパシティを超えて要素を追加する場合、
- スライスが参照していた配列とは別に、新たな配列を作りそれを参照する
- 新しく作られる配列の長さはキャパシティの2倍にする
という振る舞いをします。
そのため今回の例では、新しく作られる配列の長さはキャパシティの2倍の4になり、
なおかつ参照する配列が変わったので、s[0]
のアドレスも変わりました。