sliceの定義パターンメモ

Sliceの定義パターンメモ

定義の方法が何種類かあるが、実際に書いてるとき毎回ググったりするのが煩わしいのでまとめる。 パターンは以下の本に記載されていたもの。この本は他にも実際に書いているとどれが正しいのか迷ってしまうようなものをケースごとにパターン分けしてまとめてくれているのでとても参考になる。

Learning Go

適切な定義

  • sliceの拡張(メモリの再確保)する回数を最小化にするように定義する
  • golangでもsliceの内部配列は拡張時に現在の2倍のサイズを確保する

定義パターン

1. var declaration with no assigned value [zero length, nil]

nil sliceを作る。

var data []int
  • length: 0
  • capacity: 0
  • nil

いつ使う?

  • まったくsliceが拡張する必要がないとき。
  • sliceからJSONに変換するときのみ有用

いきなり↑の意味がわからなかった😢

2. slice literal [non-zero length, not nil]

data := []int{2,4,6,8} // numbers we appreciate

いつ使う?

  • 初期値があるとき
  • sliceの値が変わることがないとき

3. make()

必要なsliceのサイズがだいたいわかっているが入る値についてはコードを書いてるときにはわからないとき。プログラム実行時にデータを取得して入れるときとか?この場合はmakeを使う。

これはすなわち以下の#1 or #2のどちらにするかという問題になる

  1. 「makeでnon-zero lengthを指定する」
  2. 「makeでzero lengthを指定する + non zero capacityを指定する」

3-1. slice as buffer [non-zero lenght, not nil]

これは#1

buf := make([]byte, 2048)

いつ使う?

  • sliceをbufferとして使うとき

3-2. non-zero lengthを指定、値の設定はindexを使う [non-zero lenght, not nil]

  • これも#1
  • 要素へのアクセスはindex
  • 注意: この定義の場合appendで要素を追加すると、末尾に追加される。0番目には追加されない
original := []int{1, 2, 3, 4, 5}
double := make([]int, 5)
fmt.Printf("original: %v length: %v, capacity: %v", original, len(original), cap(original))
fmt.Printf("double: %v length: %v, capacity: %v", double, len(double), cap(double))
for i, v := range original {
    double[i] = 2 * v 
}
fmt.Printf("double: %v length: %v, capacity: %v", double, len(double), cap(double))

いつ使う?

  • あるsliceの値を変換した値を入れるsliceを用意するとき

デメリット

  • lengthの指定を間違えると、必要なサイズより大きい場合は無駄なzero valueがsliceの余剰部分に残る、または小さい場合はindex指定でout of rangeでpanicが起きる

3-3. zero lengthを指定、non-zero capacityを指定 [zero length, not nil]

これは#2

a := make([]int, 0, 5)

いつ使う?

  • 上記以外のシチュエーション

メリット

  • 必要なサイズが想定より小さい場合に無駄なzero valueが入らない、逆に大きい場合もindex指定と違ってout of rangeでpanicが起きることもない

結論

goのコミュニティでは3-2派と3-3派で分かれているらしい。 本書では、パフォーマンス的には遅くなる可能性があるが、バグの原因となる可能性があるため安全面から3-3のzero lengthでappendで値を追加していく方を推奨している

// 3-2
x := make([]int, 5) // [0 0 0 0 0] length: 5, capacity: 5
x[0] = 10

// 3-3
y := make([]int, 0, 5) // [] length: 0, capacity: 5
y = append(y, 10)

zero length vs zero length + capacity指定

ちなみにcapacityを指定するメリット capacityを指定していないsliceは0->1->2->4->8と4回拡張している。逆にサイズ指定しているsliceは0回

a := make([]int, 0, 5)
var b []int
fmt.Printf("a: %v length: %v, capacity: %v", a, len(a), cap(a))
fmt.Printf("b: %v length: %v, capacity: %v", b, len(b), cap(b))
for i := 1; i < 6; i++ {
    a = append(a, i)
    b = append(b, i)
}
fmt.Printf("a: %v length: %v, capacity: %v", a, len(a), cap(a))
fmt.Printf("b: %v length: %v, capacity: %v", b, len(b), cap(b))
a: [] length: 0, capacity: 5
b: [] length: 0, capacity: 0
a: [1 2 3 4 5] length: 5, capacity: 5
b: [1 2 3 4 5] length: 5, capacity: 8