配列とスライスを扱う

Go 言語の配列は固定長ですが、スライスを組み合わせて使用することで、可変長配列のように扱うことができます。

配列定義の基本 ([n], […])

Go で配列を定義するときは、変数の型名の前に [サイズ] プレフィックスを付けて定義します。 例えば、サイズ 3 の int 配列を定義するには次のようにします。

var arr [3]int

variable arr is array 3 of int. と自然な英文として読めるような文法になっています。 配列の各要素には、Java や C/C++ のように 0 から始まるインデックスを指定してアクセスすることができます。

var arr [3]int
arr[0] = 100
arr[1] = 200
arr[2] = 300
fmt.Println(arr)  //=> [100 200 300]

// 下記はビルドエラー (invalid argument: array index 3 out of bounds [0:3])
// arr[3] = 400

次のようにすれば、配列定義と同時に 初期値 を設定することができます。

var arr = [3]int{100, 200, 300}
// 関数内であれば次のように書ける
arr := [3]int{100, 200, 300}

初期値と指定する要素の数と同じサイズの配列を定義するのであれば、配列サイズを下記のように ... と指定することができます。

var arr = [...]int{100, 200, 300}
// 関数内であれば次のように書ける
arr := [...]int{100, 200, 300}

Go では、サイズの異なる配列は、型が異なるとみなします。 固定サイズの配列をパラメータにとる関数は、同じサイズの配列のみ受け取ることができます。

func myfunc(arr [3]int) {
	//...
}

func main() {
	arr1 := [3]int{100, 200, 300}
	arr2 := [4]int{100, 200, 300, 400}
	myfunc(arr1)
	myfunc(arr2) // Error: cannot use arr2 (type [4]int) as type [3]int
}

なお、上記のように固定サイズの配列を受け取る関数に配列を渡すと、値渡しで配列が渡されます(値がコピーされる)。 従って、関数内部で配列要素を書き換えても、呼び出し側の配列は変化しません。 呼び出し側の配列の内容を変更するには、後述のスライスを使用します。

配列の各要素を for ループで処理する (range)

for ループで range キーワードを使用すると、配列要素のインデックスと値を 1 つずつ取り出しながら処理することができます。

arr := [...]int{100, 200, 300}
for idx, val := range arr {
	fmt.Printf("arr[%d] = %d\n", idx, val)
}
実行結果
arr[0] = 100
arr[1] = 200
arr[2] = 300

ループ時にインデックスだけが必要な場合は、2番目のパラメータを省略して次のように記述します。

arr := [...]int{100, 200, 300}
for idx := range arr {
	fmt.Printf("arr[%d] = %d\n", idx, arr[idx])
}

値だけを取得したいときは、次のように1番目のパラメータに _ を指定して無視するようにします。

arr := [...]int{100, 200, 300}
sum := 0
for _, val := range arr {
	sum += val
}
fmt.Println(sum)  //=> 600

スライス

スライスの基本 ([])

Go 言語の配列は固定長ですが、スライスという型を可変長配列のように扱うことができます。 Go 言語では、メモリ効率が重視されるとき以外は、配列よりもスライスの方がよく使用されるようです。 スライスを定義するには、下記のようにします(配列のサイズを指定しないような構文で定義します)。 配列と同様に、初期値とする要素を設定することもできます。

var s []int                   // 初期値なし
var s = []int{100, 200, 300}  // 初期値あり
s := []int{100, 200, 300}     // 初期値あり(関数内ならこう書ける)

スライスの要素を追加するには、組込みの append 関数を使用します。 複数の要素をまとめて追加することもできます。

var s []int
s = append(s, 100)
s = append(s, 200)
s = append(s, 300, 400)
fmt.Println(s)  //=> [100 200 300 400]

スライスのサイズを拡張していく過程で、メモリ領域が再割り当てされて参照位置が変わる可能性があるので、append 関数による拡張結果は戻り値として受け取る必要があります。

スライスの要素数と容量 (len, cap)

スライスは内部データとして、現在格納されている要素数 (len) と、メモリ上に確保された容量 (cap) の情報を持っています。 それぞれの値は、len(s)cap(s) のような組込関数を使って取得することができます。 スライスの初期化時には要素数と容量は等しくなっており、append 関数などで要素の追加を行った際に容量オーバーすると、自動的に2倍の容量が新しいメモリ領域に割り当てられます(ただし、容量が 1024 が超えるあたりから、確保サイズの計算方法が変化するようです)。 下記のテストコードで、要素数 (len) と容量 (cap) の変化を確かめてみてください。

var s []int
for i := 0; i < 10; i++ {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
	s = append(s, i)
}
実行結果
len=0 cap=0 []
len=1 cap=1 [0]
len=2 cap=2 [0 1]
len=3 cap=4 [0 1 2]
len=4 cap=4 [0 1 2 3]
len=5 cap=8 [0 1 2 3 4]
len=6 cap=8 [0 1 2 3 4 5]
len=7 cap=8 [0 1 2 3 4 5 6]
len=8 cap=8 [0 1 2 3 4 5 6 7]
len=9 cap=16 [0 1 2 3 4 5 6 7 8]

確かに、容量 (cap) は2倍、2倍と拡張されていることがわかります。 容量の拡張時には、内部で新しいメモリ領域へのデータコピーが発生するため、頻繁な容量拡張が発生すると効率が悪くなります。 あらかじめ追加するおおよその要素数が分かっている場合は、後述の make 関数を使うことで、容量を指定したスライス生成を行えます。

make によるスライスの初期化

Go 言語の組込み関数の make を使用してスライスを作成すると、初期要素数 (len) と、初期容量 (cap) を指定してスライスを作成できます。 make 関数の第 1 引数には生成するスライスの型([]int など)、第 2 引数には初期要素数 (len)、第 3 引数は初期容量 (cap) を指定します。 第 3 引数の初期容量 (cap) は省略可能で、省略すると初期要素数 (len) と同じサイズになります。 各要素はゼロ値で初期化 されます。

len=cap=5 のスライスを作成
s := make([]int, 5)
fmt.Printf("len=%d cap=%d %v", len(s), cap(s), s)  //=> len=5 cap=5 [0 0 0 0 0]

次の例では、初期要素数 (len) と異なる初期容量 (cap) を指定してスライスを作成しています。 初期容量 (cap) は、初期要素数 (len) 以上の値を指定する必要があります。

len=0、cap=100 のスライスを作成
s := make([]int, 0, 100)
fmt.Printf("len=%d cap=%d %v", len(s), cap(s), s)  //=> len=0 cap=100 []

あらかじめ追加する要素数が分かっている場合は、このように初期容量を指定してスライスを生成した方が、パフォーマンスの面で有利です。 実際にどの程度の速度差が出るかは、ベンチマーク機能 を使って調べることができます。

スライスは参照

スライス変数は、配列とは異なり、内部的なメモリ領域へのアドレス値を格納しています(Java の参照のように扱えます)。 つまり、スライス変数の代入は、同じメモリ領域を参照するように指示していることになります。 下記の例で、スライス s1s2 が保持する要素は、同じメモリ領域を共有しているため、どちらか一方で要素を変更すると、もう一方のスライスの要素も変更されます。

s1 := []string{"AAA", "BBB", "CCC"}
s2 := s1       // s1 と s2 は同じ要素群を参照する
s1[0] = "XXX"  // s1 の変更は s2 にも影響する
fmt.Printf("s1=%v, s2=%v\n", s1, s2)  //=> s1=[XXX BBB CCC], s2=[XXX BBB CCC]

この性質を利用して、関数のパラメータとしてスライスを受け取り、呼び出し側のスライスの要素を書き換えることができます。

スライスの内容を変更する関数
func double(values []int) {
	for i := 0; i < len(values); i++ {
		values[i] *= 2
	}
}

func main() {
	s := []int{100, 200, 300}
	double(s)
	fmt.Println(s)  //=> [200 400 600]
}

ちなみに、上記の double 関数の中の for ループを、次のように range を使用するように変更すると、スライスの要素の値は変更されないことに注意してください。

間違った実装
func double(values []int) {
	for _, v := range values {
		v *= 2  // ローカル変数の v の値を書き換えているだけ
	}
}

配列、スライスの要素を切り出す

任意の配列やスライス(あるいは string)を、arr[m:n](m と n は数値)という形で参照すると、元の配列の「m ~ n-1」の領域の要素を参照可能なスライスを取得することができます。

img-001.drawio.svg

開始インデックスを省略した場合は先頭要素からの切り出し (s[:2] == s[0:2])、終了インデックスを省略した場合は末尾要素までの切り出し (s[2:] == s[2:len(s)]) として扱われます。

s := []int{0, 1, 2, 3, 4, 5}
s1 := s[1:4]  //=> [1, 2, 3]
s2 := s[4:]   //=> [4, 5]
s3 := s[:2]   //=> [0, 1]
s4 := s[:]    //=> [0, 1, 2, 3, 4, 5]

切り出し後のスライスは、元の配列やスライスのメモリ領域を共有する ことに注意してください。 切り出し後のスライス経由で要素の値を変更すると、元のスライスにも影響を与えます。 下記のように、それぞれのスライスのアドレスを表示してみると、同じメモリ領域を共有していることが分かります(s1s2 は先頭要素の位置がずれている分だけアドレスもずれています)。

s := []int{0, 1, 2, 3, 4, 5}
s1 := s[1:4]  //=> [1, 2, 3]
s2 := s[4:]   //=> [4, 5]
s3 := s[:2]   //=> [0, 1]
s4 := s[:]    //=> [0, 1, 2, 3, 4, 5]
fmt.Printf("%p\n", s)   // 0xc0000a8060
fmt.Printf("%p\n", s1)  // 0xc0000a8068
fmt.Printf("%p\n", s2)  // 0xc0000a8080
fmt.Printf("%p\n", s3)  // 0xc0000a8060
fmt.Printf("%p\n", s4)  // 0xc0000a8060

確実に別のメモリ領域を扱うスライスを作成したい場合は、組み込み関数 copy を使用してスライスをコピーします。

スライスをコピーする (copy)

Go の組み込み関数 copy を使用すると、あるスライスの内容を、別スライスの領域へコピーすることができます。 関数の定義は下記のようになっており、

copy(dst, src []T) int

コピー元スライス src から、コピー先スライス dst に実際にコピーされた要素数が返されます。 ただし、コピー元スライスの要素数 (len(src))、あるいは、コピー先のスライスの要素数 (len(src)) のうち小さい方の数だけしかコピーされません。 つまり、同じ要素を持つスライスを作成するには、あらかじめコピー元スライスと同じ要素数のスライスを make 関数で確保しておく必要があります。 下記のサンプルでは、src スライスをコピーして、同じ要素を持つ dst スライスを作成しています。

スライス src の要素を dst へコピー
src := []int{100, 200, 300}
dst := make([]int, len(src))  // コピー先スライスの要素数を確保 (len=3)
n := copy(dst, src)

fmt.Printf("%d elements have been copied\n", n)
fmt.Printf("src=%v, dst=%v\n", src, dst)
実行結果
3 elements have been copied
src=[100 200 300], dst=[100 200 300]

コピーされる要素数を決定するための判断基準は、あくまでスライスの要素数 (len) であって、容量 (cap) ではないことに注意してください。 なので、次のように容量だけを確保してもコピーは実行されません。

dst := make([]int, 0, 3)  // len=0, cap=3

s[m:n] 形式での要素の切り出しを組み合わせて使用すれば、スライスの部分的なコピーが可能になります。

src のインデックス 1~(3-1) の要素を dst のインデックス 2 以降にコピー
src := []int{100, 200, 300, 400, 500}
dst := []int{1, 2, 3, 4, 5}
n := copy(dst[2:], src[1:3])

fmt.Printf("%d elements have been copied\n", n)
fmt.Printf("src=%v, dst=%v\n", src, dst)
実行結果
2 elements have been copied
src=[100 200 300 400 500], dst=[1 2 200 300 5]

さらに、copy 関数の仕様として、コピー元とコピー先の領域がオーバーラップすることが許されているので、次のように、自分自身のスライスの領域間でコピーすることもできます。

src := []int{1, 2, 3, 4, 5, 6, 7, 8}
copy(src[4:], src[2:])
fmt.Println(src)  //=> [1, 2, 3, 4, 3, 4, 5, 6]

スライス同士を結合する (append)

Go の組み込み関数 append を使用すると、スライスに対して要素を追加することができますが、別のスライスの要素をすべて結合してしまうこともできます。 結合された結果のスライスは、戻り値として受け取る必要があることに注意してください。

s1 := []int{100, 200, 300}
s2 := []int{400, 500, 600}
s3 := append(s1, s2...)  //=> [100, 200, 300, 400, 500, 600]

上記の例では、結合結果を新しいスライス s3 に割り当てていますが、もちろん既存のスライス s1 に上書き代入してしまうこともできます。

s1 := []int{100, 200, 300}
s2 := []int{400, 500, 600}
s1 = append(s1, s2...)  //=> [100, 200, 300, 400, 500, 600]

このケースでは、append 後に新しいメモリ領域が確保されるため、代入後の s1 のアドレスは、元の s1 のアドレスから変化していることに注意してください。