質問の背景:
スライスは、Goにおいて固定長配列の代替として登場した主要な動的データ構造の一つで、利便性とメモリの効率を向上させます。スライスは配列の部分集合を柔軟に操作できる一方で、パフォーマンスと安全性のために重要な細かい仕様があります。
問題:
多くの開発者は、スライスがどのように構成されているのかを理解していません。スライスは配列そのものではなく、配列へのポインタ、長さ、キャパシティを持つ構造体です。これにより、メモリリークやコピーを扱う際のバグ、元の配列を変更した際の予期しない影響が生じることがあります。
解決策:
スライスは次のような型です:
type slice struct { ptr unsafe.Pointer len int cap int }
append()を使用してスライスを拡張すると、バックアップ配列が再割り当てされる可能性があり、以前の古い配列に対するすべての参照は有効ですが、古いデータを指し示します。この特性を知らないと、エラーやメモリリークが発生します。
メモリの正しい割り当てとコピーの例:
src := []int{1,2,3,4,5} dst := make([]int, len(src)) copy(dst, src)
[:]で作成されたスライスは、基になる配列を共有し、それらの変更は相互に影響を与えます。コピーが行われていない場合です。
主な特徴:
他のスライスが同じ配列にリンクしている場合、appendでキャパシティを超えてスライスを拡張するとどうなりますか?
キャパシティを超えたappendは、新しいメモリ配置を持つ基になる配列を作成し、このスライスだけが新しい配列を指します。他のスライスは古い配列を指します。これはデータの不一致の一般的な原因です。
大きな配列から得た小さなスライスを長期間保持しないことが重要な理由は何ですか?
スライスが非常に小さくても、そのポインタはバックアップ配列全体への参照を保持しているため、大きな配列がメモリに保持されてしまい、メモリリークの原因となる可能性があります。
配列の境界を越えてスライスするとどうなりますか?
panicが発生します: runtime error: slice bounds out of range.
関数は大きなファイルをバイトの配列に読み込み、最初の100要素のスライスを返します。このスライスは長期間保持されますが、大きな配列のためのメモリはGCに残っています。
利点:
欠点:
スライスを取得した直後に、必要な部分を新しいスライスにmakeとcopyでコピーします。古い配列はすぐに忘れられ、GCがメモリを解放します。
利点:
欠点: