はじめに
本記事は以下のツイートの翻訳転載です。
スライスの初期化についての内容になります。
パフォーマンスを重視したスライスの初期化1
なぜパフォーマンスを考慮する必要があるのかについては次のPostを参照してください。
以前は、make(a, 10)
を使って初期化していました。
が、結果的に先頭に意図しないゼロを大量に含んでしまうappend()
(画像参照)を頻繁に手癖で書いてしまっていました。
これを避けるために、現在ではより効果的な初期化の方法である make(a, 0, 10)
を使うようにしています。
おわりに
make
でのスライスの初期化について
Goのスライスには長さ(length
)と容量(capacity
)という概念が存在しています。
make
を使用してスライスを初期化する場合、make([type], [length], [capacity])
のように指定します。
func main() {
a := make([]int, 5)
fmt.Printf("a (length: %d, capacity: %d)\n=> %v\n", len(a), cap(a), a)
// a (length: 5, capacity: 5)
// => [0 0 0 0 0]
b := make([]int, 0, 5)
fmt.Printf("b (length: %d, capacity: %d)\n=> %v\n", len(b), cap(b), b)
// b (length: 0, capacity: 5)
// => []
c := make([]int, 0)
fmt.Printf("c (length: %d, capacity: %d)\n=> %v\n", len(c), cap(c), c)
// c (length: 0, capacity: 0)
// => []
}
はじめから長さを持っているスライスにappend
してしまうと、初期化時の長さの分だけ先頭にゼロ(ゼロ値)を含むスライスができてしまうわけです。
JavaScriptでも同じような挙動になります。
const arr = Array.from({ length: 5 });
arr.push(5);
console.log(arr);
// [undefined, undefined, undefined, undefined, undefined, 5]
スライスの容量について
長さについてJavaScriptと比較しましたが、容量については、JavaScriptには無い概念です。
スライスの長さの変更に比べて、容量に変更が発生した場合の負荷は大きいです。
4人乗りの車を家族4人で乗り回してるうちは良いですが、家族が5人になってしまうと車自体を買い替えないといけないので大変です。
パフォーマンスを考慮すると、スライスの容量に変更がないように初期化するべきではあるのですが、
長さの変更に比べて負荷がかかるとはいえ、微々たるものではあるので、ごりごり開発を進める段階においてスライスの初期化は
func main() {
var c []int
}
としておいて、
リリース前に余裕がある場合はリファクタすれば良し、余裕がなければ問題が顕在化するまで放置で良しで良いと思っています。
※例外的に、以下のような場合はmake
で長さと容量を指定した初期化を行います。
func main() {
src := []int{1, 2, 3, 4, 5}
dst := SliceMap(src, func(v int) string {
return fmt.Sprintf("value: %d", v)
})
fmt.Printf("dst: %v\n", dst)
// dst: [value: 1 value: 2 value: 3 value: 4 value: 5]
}
// SliceMap はJavaScriptのArray.prototype.mapのようなものです。
func SliceMap[E1 any, E2 any](src []E1, f func(E1) E2) []E2 {
// スライスの長さと容量が明らかに決定している場合、かつスコープが短い場合はmakeを使う
dst := make([]E2, len(src), cap(src))
for i, v := range src {
dst[i] = f(v)
}
return dst
}
-
pre-allocation
↩