まくまくHugo/Goノート
配列とスライスを扱う
2017-09-01
Go 言語の配列は固定長ですが、スライスを組み合わせて使用することで、可変長配列のように扱うことができます。

配列定義の基本

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

var arr [3]int

“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]

下記のようにすれば、配列定義と同時に初期値を設定することができます(後者の記述方法ができるのは関数内のみ)。

var arr = [3]int{100, 200, 300}
arr := [3]int{100, 200, 300}

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

var arr = [...]int{100, 200, 300}
arr := [...]int{100, 200, 300}

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

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

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

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

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

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

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

実行結果

arr[0] = 100
arr[1] = 200
arr[2] = 300

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

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

値だけを取得したいときは、次のように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(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) を指定してスライスを作成できます。 初期サイズを 1 以上に設定した場合、各要素はゼロ値で初期化されます。

作成したいスライスの型と、初期サイズ (len) だけを指定して make`を呼び出すと、初期容量 (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) 以上の値を指定する必要があります。

初期サイズ=5、初期容量=100 のスライスを作成

s := make([]int, 5, 100)
fmt.Printf("len=%d cap=%d %v", len(s), cap(s), s)  //=> len=5 cap=100 [0 0 0 0 0]

スライスは参照

スライス変数は、配列とは異なり、内部的なメモリ領域へのアドレス値を格納しています(Java の参照のようなもの)。 つまり、スライス変数の代入は、同じメモリ領域を参照するように指示していることになります。

下記の例では、スライス s1s2 は同じメモリ領域を共有しているため、どちらか一方の変更が、もう一方にも影響を与えることを示しています。

s1 := []string{"AAA", "BBB", "CCC"}
s2 := s1       // 同じ領域を参照する
s1[0] = "XXX"
fmt.Printf("s1=%v, s2=%v\n", s1, s2)  //=> s1=[XXX BBB CCC], s2=[XXX BBB CCC]

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

func change(values []int) {
	for i := 0; i < len(values); i++ {
		values[i] *= 2
	}
}

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

ちなみに、上記の change 関数の中の for ループを、次のように range を使用するように変更すると、スライスの要素の値は変更されません。

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

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

任意の配列やスライス(あるいは string)を、arr[開始インデックス:終了インデックス] という形で参照すると、元の配列の「開始インデックス ~ 終了インデックス-1」 の領域の要素を参照可能なスライスを取得することができます。 開始インデックスを省略した場合は先頭要素からの切り出し (s[:3] == s[0:3])、終了インデックスを省略した場合は末尾要素までの切り出し (s[3:] == s[3:len(3)]) として扱われます。

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

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

s := []int{1, 2, 3, 4, 5, 6, 7}
s1 := s[2:5]  //=> [3, 4, 5]
s2 := s[5:]   //=> [6, 7]
s3 := s[:2]   //=> [1, 2]
s4 := s[:]    //=> [1, 2, 3, 4, 5, 6, 7]
fmt.Printf("%p\n", s)   // 0xc04200c240
fmt.Printf("%p\n", s1)  // 0xc04200c250
fmt.Printf("%p\n", s2)  // 0xc04200c268
fmt.Printf("%p\n", s3)  // 0xc04200c240
fmt.Printf("%p\n", s4)  // 0xc04200c240

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

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

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

copy(dst, src []T) int

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

例: スライス src の要素を dst へコピー

func main() {
	src := []int{100, 200, 300}
	dst := make([]int, 3)  // コピー先スライスの要素数を確保 (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

スライスによる要素の切り出しを組み合わせて使用すれば、任意の位置の要素を、任意の位置にコピーすることができます。

例: src のインデックス 1~(3-1) の要素を dst のインデックス 2 以降にコピー

func main() {
	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 のアドレスから変化していることに注意してください。

2017-09-01