Go 言語のスライス

Go 言語のスライスとは?

Go にはスライス (slice) というデータ構造が用意されています。

スライスは配列と似ていますが、最も特徴的な点は、Go の配列は宣言した時点で型とサイズが決められているのに対して、「スライスはサイズが決められていない」という点でしょう。 スライスは必要に応じてデータ領域を割り当て直すことで、動的な配列として振る舞います。

スライスを学ぶ時には、配列との相違点を把握することで理解が深まりますので、「Go 言語の配列」も参考にしてください。

スライスは、配列と同様に次のように宣言できます。

a := []int{1, 2, 3} // [3] や [...] などと書く配列と違い、[] と書く
fmt.Println(a) // [1 2 3]
fmt.Println(a[0]) // 1

配列の宣言とよく似ていますが、サイズの指定がない点が異なります。

配列と同様、スライスの要素にはインデックスでアクセスできます。

Go のスライスの長さとキャパシティ

スライスの 「長さ」とは、スライスの中で値がセットされている要素数のことです。ビルトイン関数 len() で取得できます。

スライスの 「キャパシティ (capacity)」 とは、スライスに割り当てらているバッファのサイズのことです。 現在のキャパシティはビルトイン関数の cap() で取得できます。

ビルトイン関数の append() を使うとスライスに要素を追加できます。 要素を追加する時に、スライスのキャパシティを超えて値の追加を試みると、新たにバッファを確保し直し、これまでの値をその新しいバッファにコピーして、新しい値を追加します。

試しに、空の int 型のスライスを用意し、そこに 1 から 10 までの数値を追加し、スライスの「長さ」と「キャパシティ」がどのように変わるかみてみましょう。

package main

import "fmt"

// スライスの情報 (内容、長さ、キャパシティ) を表示する関数
func printInfo(a []int) {
	s := fmt.Sprintf("%v", a)
	fmt.Printf("%-22s %3d %3d\n", s, len(a), cap(a))
}

// main 関数
func main() {
	a := []int{} // 空のスライス (nil)

	// ヘッダの表示
	fmt.Printf("%-22s len cap\n", "values")
	// スライスに 1 から 10 までの数字を追加
	for i := 0; i < 10; i++ {
		a = append(a, i+1) // 値を追加
		printInfo(a) // 情報の表示
	}
}
main.go

実行結果は次のようになりました。

go run main.go
values                 len cap
[1]                      1   1
[1 2]                    2   2
[1 2 3]                  3   4
[1 2 3 4]                4   4
[1 2 3 4 5]              5   8
[1 2 3 4 5 6]            6   8
[1 2 3 4 5 6 7]          7   8
[1 2 3 4 5 6 7 8]        8   8
[1 2 3 4 5 6 7 8 9]      9  16
[1 2 3 4 5 6 7 8 9 10]  10  16

キャパシティの数を超えて要素を追加すると、自動的にキャパシティが増加していることがわかりますね。

Go のスライスの作成

配列と同様のスライスの作成方法

スライスは配列とほぼ同様に宣言が可能です。

a := []int{}
// [] 長さ:0, キャパシティ:0

b := []int{10, 20, 30}
// [10 20 30] 長さ:3, キャパシティ:3

c := []int{1: 10, 5: 50, 7: 70}
// [0 10 0 0 0 50 0 70] 長さ:8, キャパシティ:8

これらの配列と同様の宣言方法については、「Go 言語の配列」を参考にしてください。ほぼ同様なのでここでは説明を省略します。スライスの場合はサイズを指定しない点が、配列と異なります。

ビルトイン関数 make() を用いたスライスの作成

スライスではこの他、ビルトイン関数の make() 関数を使ってスライスを作る方法があります。

make() 関数では次の形式で、スライスを作成できます。

変数名 := make([]データ型, 長さ, キャパシティ)

型と長さは必須の引数ですが、キャパシティは任意です。キャパシティを省略した場合は長さと同じ値になります。

長さのみを指定して、make() 関数でスライスを作る例は次のようになります。

a := make([]int, 5)
// [0 0 0 0 0] 長さ: 5, キャパシティ: 5

この場合、長さで指定した箇所はそれぞれのデータ型の既定値で初期化されます。ここでは int 型のスライス []int であるため、 0 で初期化されています。

初期化が不要である場合には、長さを 0 としてキャパシティを指定します。

a := make([]int, 0, 5)
// [] 長さ:0, キャパシティ: 5

後述のように、 append() 関数を用いてスライスに要素を追加する場合、キャパシティを超えた要素数の追加を試みるとバッファの割り当て直し及び値のコピーなどのオーバーヘットが発生します。このため、要素を追加することがあらかじめわかっている場合は、キャパシティに余裕を持っておく方が良いです。

Go のスライスに要素を追加

配列は固定サイズですが、スライスには動的に要素を追加することができます。

スライスへ要素を追加するには、ビルトイン関数の append を使います。

fruits := []string{"Apple", "Banana", "Orange"}
fmt.Println(fruits) // [Apple Banana Orange]
fruits = append(fruits, "Mango", "Strawberry") // Mango と Strawberry を追加
fmt.Println(fruits) // [Apple Banana Orange Mango Strawberry]

あるスライスに、もう一つのスライスの内容を全て追加するには ... を用いて、次のように記載します。

a := []int{1, 2, 3}
fmt.Println(a) // [1 2 3]
b := []int{5, 7, 9}
a = append(a, b...) // b の内容を全て a に追加
fmt.Println(a) // [1 2 3 5 7 9]

Go のスライス式

Go のスライス式の基本的な書き方

スライスのインデックスに [low : high] という形式を用いることで、元のスライスの一部の要素を取り出した「部分」スライスを取得できます。

このインデックス表記をスライス式 (slice expression) といいます。

スライス a に対して、 a[low : high] とした場合、インデックス low から high の手前までの要素をもつスライスを返します。low を省略した場合は「0」、 high を省略した場合は「サイズ」を指定したことと同じになります。

具体例を見てみましょう。

a := []int{0, 1, 2, 3, 4, 5, 6, 7}
fmt.Println(a[2:5]) // [2 3 4]
fmt.Println(a[:3])  // [0 1 2]
fmt.Println(a[3:])  // [3 4 5 6 7]

スライス式で取得したスライスはバッファを共有

スライス式で部分スライスを取り出した場合、同じスライスの一部を返す点に注意する必要があります。元のスライスと、返されたスライスが参照しているバッファは同じです。

a := []int{0, 1, 2, 3, 4, 5}
b := a[3:] // b は [3 4 5]
b[1] = 100 // b[1] は a[4] と同じ場所を指す
fmt.Println(a) // [0 1 2 3 100 5]

Go のスライス

同じバッファを参照しているために、インデックスは違えど、値が同じスライスが取得できていることになります。

このため、 上のコードで b[1] = 100 としたときに、 a[4]100 に書き変わっています。

バッファが共有されない場合

上で説明したように、スライス式で取り出したスライスは、元のスライスとバッファを共有するため、取り出したスライスに対して行った変更は元のスライスからも参照できます。

ところが、次の例のように append() 関数で要素を追加してバッファの割り当て直しが発生した後には、元のバッファと異なるバッファを参照することになります。

a := []int{1, 2, 3}
b := a[1:]
fmt.Println("a=", a, "b=", b)
// 初期状態: a= [1 2 3] b= [2 3]

b[0] = 99 // b[0] に 99 を代入
fmt.Println("a=", a, "b=", b)
// a= [1 99 3] b= [99 3]
// a[1] が書き変わっている

b = append(b, 4) // b に要素を追加
b[0] = 88 // b[0] に 88 を代入
fmt.Println("a=", a, "b=", b)
// a= [1 99 3] b= [88 3 4]
// b[0] は 88 に書き変わっているが a は変わらない。

Go の配列をスライスに変換

スライス式を使うことで、配列をスライスとして扱えるようになります。

スライス式の lowhigh を省略した [:] を配列のインデックスに適用することで、 同じバッファを参照する (従って、同じ値をもつ) スライスを取得できます。

a := [...]int{0, 1, 2}
b := a[:]
fmt.Printf("a:%T   b:%T\n", a, b)
// a:[3]int   b:[]int

配列がスライスの作成元の場合も、バッファの差し替えが発生することで、元のバッファを参照したりしなかったりする問題が発生するので注意が必要です。

Go のスライス同士の比較

Go の配列では、要素が等しいかどうかチェックするために ==演算子が使えました。 しかし、スライスでは比較演算子を用いた比較は定義されておらず、比較の操作をすることはできません。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Go 言語入門