search
LoginSignup
62

More than 3 years have passed since last update.

posted at

updated at

Organization

Go言語のスライスで勘違いしやすいこところ

Go言語のスライスで勘違いしやすいこところ

環境

go version go1.9.4
openSUSE Leap 42.3

概要

Go言語やっててスライスという参照型に遭遇しました。
個人的に勘違いしやすい所があったので、ここに自分用の備忘録としてまとめておきます。

スライスと配列の違い

配列のような挙動をする参照型がスライスです。なので配列とは全然違います。
一緒のものと考えてはいけません。配列は、固定でありスライスは動的であると考えます。

スライスと配列の定義

スライスが混乱しやすい要因の一つとして個人的に挙げられるのは定義です。
以下に、配列とスライスの定義を列挙しました。

スライスの定義

 test := []int{1, 2, 3} //要素数3 容量3のスライス
 test := make([]int, 3, 3) //要素数3 要素数3のスライス

配列の定義

 var test[]                //要素数0  容量0の配列
 var test[10]              //要素数10 容量10の配列
 test := [3]int{1, 2, 3}   //要素数3 容量3の配列
 test := [...]int{1, 2, 3} //要素数3 容量3の配列

混乱しやすい所としては、 :=演算子を使った定義によるものでしょう。
ブロックの中に数値を入れるか入れないかの違いで、挙動が変わります。

appendによる挙動の違い

配列は固定なので拡張不可ですが、スライスは動的なので拡張可能です。
appendという要素を追加する関数を使った検証プログラムを書いてみましょう。

array
package main

import(
    "fmt"
)

func main(){
    test := [3]int{1, 2, 3}
    test = append(test, 5) 
    fmt.Println(test)
}
実行結果
./array.go:9:15: first argument to append must be slice; have [3]int
exit status 2
Process exiting with code: 1

上のarrayプログラムはエラーになってしまい実行できません。
何度も書くように配列は固定なのでappendを使った要素の拡張ができないのです。

では、sliceプログラムの場合はどうなるでしょうか。

slice
package main

import(
    "fmt"
)

func main(){
    test := []int{1, 2, 3}
    test = append(test, 5) 
    fmt.Println(test)
}
実行結果
[1 2 3 5]

見事に5の数値が追加されていることがわかります。
これで配列とスライスが完全に別物であるということが認識できました。

要素数と容量

スライスと配列には、要素数と容量という概念が存在します。
スライスの要素数の取得にはlen関数を使用します。
スライスに対して確保しているメモリ領域の取得にはcap関数を使います。

lengthとcapacity
package main

import(
    "fmt"
)

func main(){
    test := []int{1, 2, 3}
    fmt.Printf("len=%d, cap=%d\n", len(test), cap(test))
}
実行結果
len=3, cap=3

容量を超えたappend

では、スライスの容量を超えた数をappendするときに一体何が行われるのでしょうか。
それは、メモリ領域の再確保です。記憶しているすべての要素を新しく拡張されたメモリ領域へコピーし、それからappend分の値を追加します。

プログラムを作成し、append時のcapについて見てみましょう。

lengthとcapacity2

package main

import (
    "fmt"
)

func main() {
    test := []int{1, 2, 3}
    fmt.Printf("len=%d, cap=%d\n", len(test), cap(test))
    test = append(test, 4)
    fmt.Printf("len=%d, cap=%d\n", len(test), cap(test))
}
実行結果
len=3, cap=3
len=4, cap=6

capが4ではないことに驚かれる方も多いでしょう。

これはGo言語の仕様によるもので、容量オーバーしたら基本的に2倍の値を確保します。
appendするたびに一々メモリ領域を確保してコピーする手順を踏むのは効率が悪いため
このような動作になっています。

簡易スライス式と完全スライス式

Go言語のスライスには[1:4]のような表現で値を取得できる機能があります。
Pythonのようだと感じた人は、多いかと思いますが、負の数値が扱えないので注意してください。

簡易スライス式

[1:]や[1:2]などの書き方をするスライス式を簡易スライス式といいます。

簡易スライス式
package main

import (
    "fmt"
)

func main() {
    test := []int{1, 2, 3, 4, 5, 6}
    test2 := test[4:]
    fmt.Println(test2)
    fmt.Printf("len=%d, cap=%d\n", len(test2), cap(test2))
}

実行結果
[5 6]
len=2, cap=2

混乱しやすいのは[0:3]や[:3]と書く場合のcapです。

簡易スライス式2
package main

import (
    "fmt"
)

func main() {
    test := []int{1, 2, 3, 4, 5, 6}
    test2 := test[:2]
    fmt.Println(test2)
    fmt.Printf("len=%d, cap=%d\n", len(test2), cap(test2))
}
実行結果
[1 2]
len=2, cap=6

capが変動しないことに驚かれる方もいるでしょう。
スライス式の左が0もしくは記述しない場合、capが変わることはありません。
元の配列分の容量を確保してくれます。

上記のスライス式の左を1にした場合のプログラムも載せておきます。

簡易スライス式3
package main

import (
    "fmt"
)

func main() {
    test := []int{1, 2, 3, 4, 5, 6}
    test2 := test[1:2]
    fmt.Println(test2)
    fmt.Printf("len=%d, cap=%d\n", len(test2), cap(test2))
}
実行結果
[2]
len=2, cap=5

cap=5になっていることが分かるかと思います。
このように左と右で容量を確保する際の挙動が違うので注意してください。

完全スライス式

[0:2:2]のような書き方をするスライス式を完全スライス式といいます。
簡易スライス式で紹介した最後のプログラム(cap=6のやつ)のような結末になるのが嫌だ
という方には完全スライス式をおすすめします。

完全スライス式
package main

import (
    "fmt"
)

func main() {
    test := []int{1, 2, 3, 4, 5, 6}
    test2 := test[:2:2]
    fmt.Println(test2)
    fmt.Printf("len=%d, cap=%d\n", len(test2), cap(test2))
}
実行結果
[1 2]
len=2, cap=2

完全スライス式で使う[x:y:z]のzの値には、max値を指定できます。
要するにスライス時に使う容量を指定できるわけです。

しかし、元の容量を超えた数を指定できないので注意してください。

完全スライス式2
package main

import (
    "fmt"
)

func main() {
    test := []int{1, 2, 3, 4, 5, 6}
    test2 := test[1:5:7]
    fmt.Println(test2)
    fmt.Printf("len=%d, cap=%d\n", len(test2), cap(test2))
}
実行結果
panic: runtime error: slice bounds out of range

容量が6のものに対して7を指定しています。
これでは、パニックエラーが出るので注意しましょう。

スライス式は参照型

スライスは参照型です。
なので、スライスを代入して作成した新しいスライスなどでappendなどを使う場合、元の値が上書きされるを考慮してプログラミングしなければありません。

上書きされる値
package main

import (
    "fmt"
)

func main() {
    test := []int{1, 2, 3, 4, 5, 6}
    test2 := test[0:2]
    fmt.Println(test)
    test2 = append(test2, 1)
    fmt.Println(test2)
    fmt.Println(test)
}
実行結果
[1 2 3 4 5 6]
[1 2 1]
[1 2 1 4 5 6]

要素で2番めの値が1という数値に上書きされていることが分かります。
これはtest2で1という数値をappendした事によるものです。

Go言語のスライスで勘違いしやすいこところ

ようやくタイトルが回収できました。
Go言語のスライスを勉強してて最も勘違いしやすいであろう箇所に関して記述します。

Go言語のスライスで勘違いしやすいところ
package main

import (
    "fmt"
)

func main() {
    test := []int{1, 2, 3, 4, 5, 6}
    test2 := append(test, []int{100, 200, 300}...)
    test2[0] = -1
    fmt.Println(test)
    fmt.Println(test2)
}
実行結果
[1 2 3 4 5 6]
[-1 2 3 4 5 6 100 200 300]

なぜ、testの0番目が-1じゃないのだと驚愕した方もいるでしょう。
Go言語は、容量オーバしたスライスに対して新しくメモリ領域を確保してコピーします。
appendした際に容量オーバしたので、testとtest2は、それぞれ別の領域を参照していることになります。
なのでtestは-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
What you can do with signing up
62