70
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

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
    
70
46
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
70
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?