概要
Sliceの構造を始め、関数で呼び出した場合の挙動やappendなどsliceを操作した場合どうなるかをまとめました。
環境
- golang v1.9.0
Sliceの構造
Sliceは以下のような3つの要素でなりたっています。
- 配列へのポインタ
- length
- capacity
図示すると以下です。
ref: Go Slices: usage and internals - go.dev
関数の引数として渡した時
Goの関数の引数は基本的に値渡しです。これはsliceも同じです。なので渡した変数のポインタは異なります。
func main() { s := []int{1, 2, 3} fmt.Printf("%p\n", &s) // 0x1040a0b0 someFunc(s) } func someFunc(s []int) { fmt.Printf("%p\n", &s) // 0x1040a0c0 で異なる }
https://play.golang.org/p/CELzuPAwna
ではなぜsliceを渡しても同じ要素にアクセスできるのでしょうか?
理由は値渡しされたsliceの持っている配列へのポインタが同じだからです。
func main() { s := []int{1, 2, 3} fmt.Printf("%p\n", &s) // 0x1040a0b0 fmt.Printf("%p\n", &s[0]) // 0x10410020 someFunc(s) } func someFunc(s []int) { fmt.Printf("%p\n", &s) // 0x1040a0c0 fmt.Printf("%p\n", &s[0]) // 0x10410020 で同じ }
https://play.golang.org/p/vxHtB9sg9N
これを理解していれば、
- 配列の要素の値を書き換えると呼び出し元のsliceにも影響する
- 非常に大きなsliceを引数で渡しても大量のコピーが走ることはない
ということが分かります。
あくまでslice自体が値渡しで、中の配列は同じものを見ています。
appendの挙動
capacityが足りない時
capacityが足りない時のappendでは配列の再配置が生じます。
func main() { s := []int{1, 2, 3} fmt.Printf("%p\n", &s) // 0x1040a0b0 fmt.Printf("%p\n", &s[0]) // 0x10410020 s = append(s, 4) fmt.Printf("%p\n", &s) // 0x1040a0b0 fmt.Printf("%p\n", &s[0]) // 0x10454000 新しい配列を指す }
https://play.golang.org/p/GB9MJvN69T
関数の引数で渡した場合
なので関数内でappendした場合は呼び出し元のsliceが見ている配列とは違うものになります。そのため関数内の配列は増えてますが、呼び出し元のsliceは変わりません。
func main() { s := []int{1, 2, 3} fmt.Printf("%p\n", &s[0]) // 0x10410020 add(s) fmt.Println(s) // [1 2 3] fmt.Printf("%p\n", &s[0]) // 0x10410020 } func add(s []int) { fmt.Printf("before: %p\n", &s[0]) // 0x10410020 s = append(s, 4) fmt.Println(s) // [1 2 3 4] fmt.Printf("after: %p\n", &s[0]) // 0x10454020 とポインタが変わる }
https://play.golang.org/p/Q6OuxDTxvG
中身の配列が新しいものになってますね。
capacityが十分なとき
capacityに余裕がある時のappendは再配置をしないので、中身が変わることはありません。
func main() { s := make([]int, 3, 4) s[0], s[1], s[2] = 1, 2, 3 fmt.Printf("%p\n", &s) // 0x1040a0b0 fmt.Printf("%p\n", &s[0]) // 0x10410020 s = append(s, 4) fmt.Printf("%p\n", &s) // 0x1040a0b0 fmt.Printf("%p\n", &s[0]) // 0x10410020 同じ配列を指す }
https://play.golang.org/p/LLbeLmcB54
関数の引数で渡した場合
では関数内で実行するとどうなるかというと、ちょっと変わった挙動になります。
func main() { s := make([]int, 3, 4) s[0], s[1], s[2] = 1, 2, 3 fmt.Printf("%p\n", &s[0]) // 0x10410020 add(s) fmt.Println(s) // [1 2 3] fmt.Printf("%p\n", &s[0]) // 0x10410020 同じ配列を指す } func add(s []int) { fmt.Printf("before: %p\n", &s[0]) // 0x10410020 s = append(s, 4) fmt.Println(s) // [1 2 3 4] fmt.Printf("after: %p\n", &s[0]) // 0x10410020 同じ配列を指す }
https://play.golang.org/p/ntq4D75py2
同じ配列を指しているのにも関わらず、printされる値が異なります。
この理由は呼び出し元のsliceのlenが更新されていないためです。
add()
関数内ではappend
によってlenも4
になりますが、呼び出し元のsliceは別オブジェクトであり、3
のままなのでこういったことが起きます。
この場合呼び出し元も更新したければ、
func main() { s := make([]int, 3, 4) s[0], s[1], s[2] = 1, 2, 3 fmt.Printf("%p\n", &s[0]) add(s) s = s[:cap(s)] // 追記 fmt.Println(s) // [1 2 3 4] fmt.Printf("%p\n", &s[0]) }
https://play.golang.org/p/vBwTvpWBdk
のように
s = s[:cap(s)]
を挟むといけます。
s[head:tail]
という形でsliceを操作する場合、tail
はcap
までという言語仕様になっています。
ただし直接s[tail-1]
みたいな要素へのアクセスはできません。
まとめ
Sliceの挙動として以下を覚えておくと良いです。
- sliceは配列へのポインタを持っている
- 引数で渡したsliceは値渡しなので別物。でも中のポインタが指している配列は同じ
- appendなどで再配置が起きると指している配列が変わる。