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)
}
ループ時にインデックスだけが必要な場合は、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)
}
確かに、容量 (cap
) は2倍、2倍と拡張されていることがわかります。
容量の拡張時には、内部で新しいメモリ領域へのデータコピーが発生するため、頻繁な容量拡張が発生すると効率が悪くなります。
あらかじめ追加するおおよその要素数が分かっている場合は、後述の make
関数を使うことで、容量を指定したスライス生成を行えます。
make によるスライスの初期化
Go 言語の組込み関数の make を使用してスライスを作成すると、初期要素数 (len
) と、初期容量 (cap
) を指定してスライスを作成できます。
make
関数の第 1 引数には生成するスライスの型([]int
など)、第 2 引数には初期要素数 (len
)、第 3 引数は初期容量 (cap
) を指定します。
第 3 引数の初期容量 (cap
) は省略可能で、省略すると初期要素数 (len
) と同じサイズになります。
各要素はゼロ値で初期化 されます。
次の例では、初期要素数 (len
) と異なる初期容量 (cap
) を指定してスライスを作成しています。
初期容量 (cap
) は、初期要素数 (len
) 以上の値を指定する必要があります。
あらかじめ追加する要素数が分かっている場合は、このように初期容量を指定してスライスを生成した方が、パフォーマンスの面で有利です。 実際にどの程度の速度差が出るかは、ベンチマーク機能 を使って調べることができます。
スライスは参照
スライス変数は、配列とは異なり、内部的なメモリ領域へのアドレス値を格納しています(Java の参照のように扱えます)。
つまり、スライス変数の代入は、同じメモリ領域を参照するように指示していることになります。
下記の例で、スライス s1
と s2
が保持する要素は、同じメモリ領域を共有しているため、どちらか一方で要素を変更すると、もう一方のスライスの要素も変更されます。
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]
この性質を利用して、関数のパラメータとしてスライスを受け取り、呼び出し側のスライスの要素を書き換えることができます。
ちなみに、上記の double
関数の中の for ループを、次のように range
を使用するように変更すると、スライスの要素の値は変更されないことに注意してください。
配列、スライスの要素を切り出す
任意の配列やスライス(あるいは string
)を、arr[m:n]
(m と n は数値)という形で参照すると、元の配列の「m ~ n-1」の領域の要素を参照可能なスライスを取得することができます。
開始インデックスを省略した場合は先頭要素からの切り出し (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]
切り出し後のスライスは、元の配列やスライスのメモリ領域を共有する ことに注意してください。
切り出し後のスライス経由で要素の値を変更すると、元のスライスにも影響を与えます。
下記のように、それぞれのスライスのアドレスを表示してみると、同じメモリ領域を共有していることが分かります(s1
と s2
は先頭要素の位置がずれている分だけアドレスもずれています)。
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
スライスを作成しています。
コピーされる要素数を決定するための判断基準は、あくまでスライスの要素数 (len
) であって、容量 (cap
) ではないことに注意してください。
なので、次のように容量だけを確保してもコピーは実行されません。
dst := make([]int, 0, 3) // len=0, cap=3
s[m:n]
形式での要素の切り出しを組み合わせて使用すれば、スライスの部分的なコピーが可能になります。
さらに、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
のアドレスから変化していることに注意してください。