Go言語の可変長配列(slice)を使う上での注意

どのようなプログラミング言語を利用していても、可変長配列はよく使います。

Go 言語においては、 `slice` という可変長配列が用意されています。

`slice` を用意するためには `make` 関数を利用します。

    length := 5
    capacity := 10
    array := make([]int, length, capacity)

`make` 関数の第 1 引数(`[]int`)が型、第 2 引数(`length`)が `長さ` 、第 3 引数(`capacity`)が `容量` を意味しています。

今回は、 `長さ` と `容量` のそれぞれが、どのような役割なのかを、サンプルコードをつかって確認しました。

# 準備

## slice の長さと容量を求める関数

`slice` の `長さ` は `len` 関数、 `容量` は `cap` 関数で求めることが出来ます。

1.  サンプルコード1:

        package main
        
        import "fmt"
        
        func main() {
                lenght := 5
                capacity := 10
                array := make([]int, lenght, capacity)
        
                fmt.Printf("初期の長さ          -> %d\n", len(array))
                fmt.Printf("初期の容量          -> %d\n", cap(array))
        }

2.  出力結果:

        初期の長さ          -> 5
        初期の容量          -> 10
    
    今後のサンプルコードでも `len` 関数と `cap` 関数を使って様子を観測します。

# slice の各要素の初期値を観測する

`slice` の初期値を、添字を使って確認します。

## 長さ分の初期値

1.  サンプルコード2:

        package main
        
        import "fmt"
        
        func main() {
        
                lenght := 5
                capacity := 10
                array := make([]int, lenght, capacity)
        
                fmt.Printf("初期の長さ          -> %d\n", len(array))
                fmt.Printf("初期の容量          -> %d\n", cap(array))
        
                fmt.Println()
                fmt.Println("長さ分の各要素")
                for i := 0; i < len(array); i++ {
                        fmt.Printf("array[%d] -> %d\n", i, array[i])
                }
        
        }

2.  出力結果:

        初期の長さ          -> 5
        初期の容量          -> 10
        
        長さ分の各要素
        array[0] -> 0
        array[1] -> 0
        array[2] -> 0
        array[3] -> 0
    
    長さ分の要素には、 `int` 型の初期値である `0` が代入されています。

## 容量分の初期値

1.  サンプルコード3:

        package main
        
        import "fmt"
        
        func main() {
                lenght := 5
                capacity := 10
                array := make([]int, lenght, capacity)
        
                fmt.Printf("初期の長さ          -> %d\n", len(array))
                fmt.Printf("初期の容量          -> %d\n", cap(array))
        
                fmt.Println()
                fmt.Println("容量分の各要素")
                for i := 0; i < cap(array); i++ {
                        fmt.Printf("array[%d] -> %d\n", i, array[i])
                }
        }

2.  出力結果:

        初期の長さ          -> 5
        初期の容量          -> 10
        
        容量分の各要素
        array[0] -> 0
        array[1] -> 0
        array[2] -> 0
        array[3] -> 0
        array[4] -> 0
        panic: runtime error: index out of range
        
        goroutine 1 [running]:
        main.main()
                /home/vagrant/go/src/sample/sample3.go:16 +0x49c
        
        goroutine 2 [runnable]:
        runtime.forcegchelper()
                /usr/local/go/src/runtime/proc.go:90
        runtime.goexit()
                /usr/local/go/src/runtime/asm_amd64.s:2232 +0x1
        
        goroutine 3 [runnable]:
        runtime.bgsweep()
                /usr/local/go/src/runtime/mgc0.go:82
        runtime.goexit()
                /usr/local/go/src/runtime/asm_amd64.s:2232 +0x1
        
        goroutine 4 [runnable]:
        runtime.runfinq()
                /usr/local/go/src/runtime/malloc.go:712
        runtime.goexit()
                /usr/local/go/src/runtime/asm_amd64.s:2232 +0x1
        exit status 2
    
    `長さ` 以上の要素に、添字を使ってアクセスした時に `pannic` が発生しています。
    
    これで添字でアクセス出来るのは、長さ分までということが分かりました。

# slice を可変長配列として利用する

`slice` に要素を追加するには `append` 関数を利用します。

`append` 関数で要素を追加すれば、可変長配列として `slice` を利用できます。

## 容量の変化

### 容量に収まる様に要素を追加したときの変化

1.  サンプルコード4:

        package main
        
        import "fmt"
        
        func main() {
                lenght := 5
                capacity := 10
                array := make([]int, lenght, capacity)
        
                fmt.Printf("初期の長さ          -> %d\n", len(array))
                fmt.Printf("初期の容量          -> %d\n", cap(array))
        
                appendCount := 3
                for i := 0; i < appendCount; i++ {
                        array = append(array, i+1)
                }
        
                fmt.Println()
                fmt.Printf("append 後の長さ     -> %d\n", len(array))
                fmt.Printf("append 後の容量     -> %d\n", cap(array))
        
                fmt.Println()
                fmt.Println("append 後の長さ分の各要素")
                for i := 0; i < len(array); i++ {
                        fmt.Printf("array[%d] -> %d\n", i, array[i])
                }
        }

2.  出力結果:

        初期の長さ          -> 5
        初期の容量          -> 10
        
        append 後の長さ     -> 8
        append 後の容量     -> 10
        
        append 後の長さ分の各要素
        array[0] -> 0
        array[1] -> 0
        array[2] -> 0
        array[3] -> 0
        array[4] -> 0
        array[5] -> 1
        array[6] -> 2
        array[7] -> 3
    
    append した分、初期の長さ分の後に追加されています。 `容量` は変化しないことが分かりました。

### 容量以上になる様に要素を追加したときの変化

1.  サンプルコード5:

        package main
        
        import "fmt"
        
        func main() {
                lenght := 5
                capacity := 10
                array := make([]int, lenght, capacity)
        
                fmt.Printf("初期の長さ          -> %d\n", len(array))
                fmt.Printf("初期の容量          -> %d\n", cap(array))
        
                appendCount := 6
                for i := 0; i < appendCount; i++ {
                        array = append(array, i+1)
                }
        
                fmt.Println()
                fmt.Printf("append 後の長さ     -> %d\n", len(array))
                fmt.Printf("append 後の容量     -> %d\n", cap(array))
        
                fmt.Println()
                fmt.Println("append 後の長さ分の各要素")
                for i := 0; i < len(array); i++ {
                        fmt.Println(i, "->", array[i])
                }
        }

2.  出力結果:

        初期の長さ          -> 5
        初期の容量          -> 10
        
        append 後の長さ     -> 11
        append 後の容量     -> 20
        
        append 後の長さ分の各要素
        0 -> 0
        1 -> 0
        2 -> 0
        3 -> 0
        4 -> 0
        5 -> 1
        6 -> 2
        7 -> 3
        8 -> 4
        9 -> 5
        10 -> 6
    
    append した分、初期の長さ分の後に追加されています。
    
    それに加え、容量が初期の `10` から `20` に増えています。
    
    これにより、初期に確保した容量とは関係なく、要素を追加出来ることが分かりました。

## アドレスの変化

`slice` のアドレスを観測することにより、容量以上に要素を追加した時の振る舞いを観測します。

### 容量に収まる様に要素を追加したときの変化

1.  サンプルコード6:

        package main
        
        import "fmt"
        
        func main() {
                lenght := 5
                capacity := 10
                array := make([]int, lenght, capacity)
        
                fmt.Printf("初期のアドレス      -> %p\n", array)
        
                appendCount := 3
                for i := 0; i < appendCount; i++ {
                        array = append(array, i+1)
                }
        
                fmt.Printf("append 後のアドレス -> %p\n", array)
        
        }

2.  出力結果:

        初期のアドレス      -> 0xc2080480a0
        append 後のアドレス -> 0xc2080480a0
    
    アドレスが同じです。

### 容量に収まる様に要素を追加したときの変化

1.  サンプルコード7:

        package main
        
        import "fmt"
        
        func main() {
                lenght := 5
                capacity := 10
                array := make([]int, lenght, capacity)
        
                fmt.Printf("初期のアドレス      -> %p\n", array)
        
                appendCount := 7
                for i := 0; i < appendCount; i++ {
                        array = append(array, i+1)
                }
        
                fmt.Printf("append 後のアドレス -> %p\n", array)
        
        }

2.  出力結果:

        初期のアドレス      -> 0xc2080480a0
        append 後のアドレス -> 0xc20804c000
    
    アドレスが変わっています。
    
    容量以上の要素数を追加するときに、 `append` 関数内部で領域の再確保が発生し、新しい領域のアドレスが戻り値として返します。
    
    データは再確保された領域にコピーされます。データの量が多いと、計算コストがかかります。

## 容量の増え方

ついでに `容量` の増え方について観測してみます。

1.  サンプルコード8:

        package main
        
        import "fmt"
        
        func main() {
                lenght := 5
                capacity := 10
                array := make([]int, lenght, capacity)
        
                fmt.Printf("初期の長さ          -> %d\n", len(array))
                fmt.Printf("初期の容量          -> %d\n", cap(array))
        
                fmt.Println()
                appendCount := 100
                for i := 0; i < appendCount; i++ {
                        array = append(array, i+1)
                        fmt.Printf("%3d 個追加後の容量  -> %d\n", i+1, cap(array))
                }
        
        }

2.  出力結果:

        初期の長さ          -> 5
        初期の容量          -> 10
        
          1 個追加後の容量  -> 10
          2 個追加後の容量  -> 10
          3 個追加後の容量  -> 10
          4 個追加後の容量  -> 10
          5 個追加後の容量  -> 10
          6 個追加後の容量  -> 20
          7 個追加後の容量  -> 20
         ...
         14 個追加後の容量  -> 20
         15 個追加後の容量  -> 20
         16 個追加後の容量  -> 40
         17 個追加後の容量  -> 40
         ...
         34 個追加後の容量  -> 40
         35 個追加後の容量  -> 40
         36 個追加後の容量  -> 80
         37 個追加後の容量  -> 80
         ...
         74 個追加後の容量  -> 80
         75 個追加後の容量  -> 80
         76 個追加後の容量  -> 160
         77 個追加後の容量  -> 160
         ...
         99 個追加後の容量  -> 160
        100 個追加後の容量  -> 160
    
    `長さ` が `容量` を超えた時に、その時の `容量` の倍の `容量` が新たに確保されることが分かりました。

# 今回分かったこと

## append 関数だけを使って要素を追加していくときには、長さは 0 に指定しておく

`append` 関数は、既存の要素の最後に新しい要素を追加します。

`make` 関数で長さを `0` 以外の値にしたとき、初期の長さ分の要素を考慮した作りする必要があります。

## 扱う要素数の検討が付くときには、要素で指定しておく

新しい要素の確保、データのコピーのコストを無視すれば、容量は気にしないい事がわかりました。

しかし、事前に扱う要素素が見当がつく場合は、最初に `make` 関数で指定しておくほうがいいです。

    array := make([]int, 0, capacity)

## 引数で slice を渡し、関数内で append 関数を利用するときには、必ず戻り値で戻す

要素数が容量を超えたとき、アドレスが新しく割り振られる事がわかりました。

したがって、関数の引数として渡した `slice` が、関数内部で `appende` 関数が使われていた場合、気をつけないと期待した効果が得られません。

1.  サンプルコード9:

        package main
        
        import "fmt"
        
        func appendData(a []int) []int {
                appendCount := 100
                for i := 0; i < appendCount; i++ {
                        a = append(a, i+1)
                }
                return a
        }
        
        func main() {
        
                lenght := 0
                capacity := 0
                array := make([]int, lenght, capacity)
        
                fmt.Printf("初期の長さ          -> %d\n", len(array))
                fmt.Printf("初期の容量          -> %d\n", cap(array))
                fmt.Printf("初期のアドレス      -> %p\n", array)
        
                newArray := appendData(array)
        
                fmt.Println()
                fmt.Printf("呼出し後の長さ      -> %d\n", len(array))
                fmt.Printf("呼出し後の容量      -> %d\n", cap(array))
                fmt.Printf("呼出し後のアドレス  -> %p\n", array)
        
                fmt.Println()
                fmt.Printf("戻り値の長さ        -> %d\n", len(newArray))
                fmt.Printf("戻り値の容量        -> %d\n", cap(newArray))
                fmt.Printf("戻り値のアドレス    -> %p\n", newArray)
        
        }

2.  出力結果:

        初期の長さ          -> 0
        初期の容量          -> 0
        初期のアドレス      -> 0x5549e0
        
        呼出し後の長さ      -> 0
        呼出し後の容量      -> 0
        呼出し後のアドレス  -> 0x5549e0
        
        戻り値の長さ        -> 100
        戻り値の容量        -> 128
        戻り値のアドレス    -> 0xc208050000