初めに
Golangの勉強を始めて配列とスライスの違い、make()を使用して作成したスライスとの違いがよくわからなかったため、配列とスライスの違いを調べてみました。
Golangには対話モードはありませんが、見やすくするために記事中のコードは対話モードのように表示しています。
定義方法
配列の定義方法
array0 := [3]int {1, 2, 3}
array1 := [...]int {1, 2, 3, 4}
サイズを指定すると配列が生成されます。
スライスの定義方法
slice0 := []int {1, 2, 3}
slice1 := make([]int, 3, 5)
サイズ未指定、またはmake()を使用するとスライスが生成されます。
appendによる要素の追加
配列への要素追加
array := [3]int{1, 2, 3}
array = append(array, 4)
>> .\append_panic.go:7:17: first argument to append must be slice; have [3]int
panicが発生してしまいました。配列は固定長なので要素の追加ができません。
スライスへの要素追加
slice := []int{1, 2, 3}
slice = append(slice, 4)
>> slice: [1 2 3 4]
スライスは可変長なので、append()による要素の追加はできます。
変数への代入
配列、スライスを作成後に変数に代入したときの挙動を見てみます。
配列の変数への代入
配列作成、代入
array0 := [...]int{1, 2, 3, 4}
array1 := array0
>> array0: [1 2 3 4]
>> array1: [1 2 3 4]
>> &array0[0]: 0xc00000a360
>> &array1[0]: 0xc00000a380
配列の変数への代入は値のコピーしますが、アドレスが異なっていることから実態は別物です。
値変更
array0[0] = 5
>> array0: [5 2 3 4]
>> array1: [1 2 3 4]
代入先とは別物になっているので、値を変更しても代入先は反映されません。
スライスの変数への代入
スライス作成、代入
slice0 := []int{1,2,3,4}
slice1 := slice0
>> slice0: [1 2 3 4]
>> slice1: [1 2 3 4]
>> &slice0[0]: 0xc000058100
>> &slice1[0]: 0xc000058100
スライスは作成時に裏側で配列を作成し、スライス自体はその配列を持つインスタンスになります。
スライス自体を代入すればスライス自身の参照がコピーされます。そのため代入先も同じ配列を参照できます。
値変更
slice0[0] = 5
>> slice0: [5 2 3 4]
>> slice1: [5 2 3 4]
slice0、slice1は同じものを参照しているため、値を変更すればどちらからでも値の変更が確認できます。
appendによる要素追加
slice2 := append(slice0, 5)
>> slice0: [5 2 3 4]
>> slice1: [5 2 3 4]
>> slice2: [5 2 3 4 5]
>> &slice0[0]: 0xc000058100
>> &slice1[0]: 0xc000058100
>> &slice2[0]: 0xc00008a100
スライスへの要素の追加をappend()で行うと、スライスが返却されます。これは新しく作成された別のスライス(裏側の配列も作成される)になります。
そのため、slice0 = append(slice0, 1)のように元の変数に代入してしまうと、先ほど代入したスライスとは別物となるので注意する必要があります。
slice0[1] = 6
slice2[2] = 6
>> slice0: [5 6 3 4]
>> slice1: [5 6 3 4]
>> slice2: [5 2 6 4 5]
要素追加後の値の変更も、別物なのでこのようになります。
makeで作成したスライスの変数への代入
makeでスライス作成、代入
slice0 := make([]int, 5, 6)
slice1 := slice0
>> slice0:[0 0 0 0 0], len:5, cap:6
>> slice1:[0 0 0 0 0], len:5, cap:6
>> &slice0[0]: 0xc000080090
>> &slice1[0]: 0xc000080090
makeでスライス作成しました。make()は第一引数がデータ型(スライス、マップ、チャネルのみ)、第二引数がサイズ、第三引数が容量になります。
makeでスライス作成後に代入を行いましたが、makeで作成しない場合と同じ挙動になります。
スライスへの追加(capacity内)
slice2 := append(slice0, 1)
>> slice0:[0 0 0 0 0], len:5, cap:6
>> slice1:[0 0 0 0 0], len:5, cap:6
>> slice2:[0 0 0 0 0 1], len:6, cap:6
>> &slice0[0]: 0xc000080090
>> &slice1[0]: 0xc000080090
>> &slice2[0]: 0xc000080090
append()を使用して容量を超えないスライスへの要素の追加をしました。この場合には返却されるスライスは、アドレスが同じことからも拡張前と同じスライスになります。
スライスへの追加(capacity超過)
slice3 := append(slice2, 2)
>> slice0:[0 0 0 0 0], len:5, cap:6
>> slice1:[0 0 0 0 0], len:5, cap:6
>> slice2:[0 0 0 0 0 1], len:6, cap:6
>> slice3:[0 0 0 0 0 1 2], len:7, cap:12
>> &slice0[0]: 0xc000080090
>> &slice1[0]: 0xc000080090
>> &slice2[0]: 0xc000080090
>> &slice3[0]: 0xc00004c060
次にappend()を使用して容量を超えるスライスへの追加をしました。この場合に返却されるスライスは、容量が拡張された新しいスライスが返却されました。
make()を使用しないスライスの要素追加と同じ挙動になりました。make()を使用しないスライスは最初はサイズと容量が同じなので、append()で要素を追加するには容量が足りないため、容量を拡張する必要があるので新しいスライスが作られます。
スライスのコピー
スライスを配列のようにコピーしたい場合はcopy()を使用すると、配列の変数への代入と同じように別のスライスが作成されます。
copy()を使用する注意点としては、コピー先のスライスのサイズをコピー元に合わせないとエラーになってしまう点です。
slice0 := []int{1, 2, 3}
slice1 := make([]int, len(slice0))
copy(slice0, slice1)
>> slice0: [1 2 3]
>> slice1: [1 2 3]
>> &slice0[0]: 0xc00000a320
>> &slice1[0]: 0xc00000a340
配列からスライスを作成
以下のようにすると配列を参照するスライスが作成されます。
array := [5]int{1, 2, 3, 4, 5}
slice := array[:]
>> reflect.TypeOf(array): [5]int
>> reflect.TypeOf(slice): []int
>> arary: [1 2 3 4 5]
>> slice: [1 2 3 4 5]
>> &array[0]: 0xc0000480c0
>> &slice[0]: 0xc0000480c0
まとめ
- 配列は固定長、スライスは可変長
- 配列はappend()で拡張できない、スライスはできる
- 配列は変数への代入でコピーが作れる、スライスはcopy()を使用しないと作れない
- スライスの変数への代入後の取り扱いには注意する必要あり