Goのarrayとsliceについて説明しようとしたときに、自分でも混乱して説明できないことがあったので、もう一度調査してまとめてみました
あとは自分が過去にsliceでハマったところも記載してます
まずはarrayを理解しよう
Go Slices: usage and internalsにも書いてありますが、sliceを理解するにはまず配列について理解する必要があります
Goの配列は値である
Goの配列は値であり、C言語のように配列の先頭要素へのポインタではありません
その為、例えば関数の引数で配列を渡したり別の変数にいれたりすると、その配列のコピーが作成されます
関数内で配列の中身を変更したい場合は、配列のポインタを渡しましょう
var a = [4]int{1, 2, 3, 4}
fmt.Printf("%#v\n", a) // [4]int{1, 2, 3, 4}
fmt.Printf("%p\n", &a) // 0xc42009c000
b := a
b[0] = 0
fmt.Printf("%#v\n", b) // [4]int{0, 2, 3, 4}
fmt.Printf("%p\n", &b) // 0xc4200161a0
fmt.Printf("%#v\n", a) // [4]int{1, 2, 3, 4}
sliceは値?ポインタ?
sliceの実体はarrayへのポインタ、しかしそれだけではない
sliceはarrayへのポインタを保持しています
しかしarrayのポインタそのものではなく、arrayのポインタ、length、capacityを保持している構造体です
type slice struct {
array unsafe.Pointer
len int
cap int
}
arrayのポインタを保持しているため、他の変数に渡して要素の値を変更したら元の要素の値も変わります
a := make([]int, 1)
fmt.Printf("%#v\n", a) // []int{0}
b := a
b[0] = 1
fmt.Printf("%#v\n", b) // []int{1}
fmt.Printf("%#v\n", a) // []int{1}
sliceの宣言
var s []T
でsliceを宣言すると、sliceの構造体の中でarrayへのポインタを保持していないのでsliceはnil
になります
この場合、slice == nil
がtrue
になるほか、jsonへエンコードしてもnull
となります
make([]T, 0)
でsliceを宣言した場合は、nil
にはなりません
jsonへエンコードすると空の配列になります
ちなみにlen()
はどちらの場合も0
になります
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
var a []int
fmt.Printf("%#v: len = %d, isNil = %v, json = %s\n", a, len(a), isNil(a), marshal(a))
// []int(nil): len = 0, isNil = true, json = null
b := make([]int, 0)
fmt.Printf("%#v: len = %d, isNil = %v, json = %s\n", b, len(b), isNil(b), marshal(b))
// []int{}: len = 0, isNil = false, json = []
}
func isNil(s []int) bool {
return s == nil
}
func marshal(s []int) string {
bytes, err := json.Marshal(s)
if err != nil {
log.Fatal(err)
}
return string(bytes)
}
capacityは指定したほうが良い?
make([]T, 0)
でcapacityを宣言しないと、capacityはlengthと同じになります
append()
でcapacityを超えても要素を追加できるから、常にcapacity省略してもいいのでは?と思いますが(私は昔そう思ってました。。。)それは間違いです
capacityを超えてappendする場合は、メモリアロケーションが発生しパフォーマンスが悪くなります
package main
import "testing"
func BenchmarkAppendNoCapacitySpecified(b *testing.B) {
s := make([]int, 0)
b.ResetTimer()
for i := 0; i < b.N; i++ {
s = append(s, i)
}
}
func BenchmarkAppendCapacitySpecified(b *testing.B) {
s := make([]int, 0, b.N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
s = append(s, i)
}
}
BenchmarkAppendNoCapacitySpecified-4 100000000 42.6 ns/op 49 B/op 0 allocs/op
BenchmarkAppendCapacitySpecified-4 2000000000 12.1 ns/op 0 B/op 0 allocs/op
ベンチマークを見てもcapacityを指定したほうがパフォーマンスが良いのは明らかです
要素数の最大値がわかっているときはcapacityを指定しましょう
まとめ
- Goのarrayは値
- Goのsliceはarrayへのポインタ、length、capacityを保持した構造体
- sliceはnilになるケースがある
- capacityはなるべく指定する