スライスと配列はGoで最もよく使われるデータ構造の1つです。似たような構文にもかかわらず、それらの構成や挙動の違いはパフォーマンス、メモリ、セマンティクスに関するエラーを引き起こす可能性があります。
問題の経緯:
Goは最初から明示的なメモリ管理モデルを選択しており、配列(arrays)は固定サイズの要素のシーケンスであり、スライス(slices)は配列の動的なビューです。このような分離により、操作コストとコードの挙動を制御することが可能になります。
問題:
主な難しさは、配列のコピー(値セマンティクス)とスライスの「参照性」の混乱です。これらの型を関数に渡したり、値を変更したりする際に誤りが頻繁に発生し、予期しない副作用を引き起こします。
解決策:
配列は常に値渡しで伝達される際にコピーされます:関数はすべての内容のコピーを受け取ります。スライスは、小さな構造体(ヘッダー)であり、配列へのポインタ、長さ、および容量を含みます。配列の内容が変更されると、スライス内の変更が外部に見えるようになります(ただし、スライス自体が関数内で新しい配列に転送されると、変更されません)。
コードの例:
func updateArray(arr [3]int) { arr[0] = 10 } func updateSlice(slc []int) { slc[0] = 10 } func main() { a := [3]int{1,2,3} b := []int{1,2,3} updateArray(a) updateSlice(b) fmt.Println(a) // [1 2 3] fmt.Println(b) // [10 2 3] }
主な特徴:
関数内でスライスの長さを変更した場合、元のスライスに影響はありますか?
いいえ、関数内でスライスの長さを変更(たとえば、slc = slc[:2])しても、ローカルコピーのヘッダーにのみ影響します。元のスライスはそのままです。
append演算子は変更したスライスを同じメモリ領域内に戻しますか?
必ずしもそうではありません。容量が不足している場合、新しい配列が作成され、新しい配列へのポインタが返されます。古い配列はそのまま残ります。
コードの例:
s := []int{1,2,3} s2 := append(s, 4, 5, 6) // s2は新しいメモリ領域にある可能性があります
スライスに配列を割り当てることはできますか、あるいはその逆はできますか?
いいえ。[]intと[5]intは異なる型です。配列をスライスとして渡すには変換arr[:]を使用する必要があります。逆は不可能です。
ジュニア開発者が配列を関数に渡してテーブルを更新する機能を実装し、変更が元の配列に適用されることを期待していました。修正は「保存されません」でした。
利点:
欠点:
関数はスライスを受け取り、変更されたコピーを明示的に返すことで、効果の予測可能性を高めました。すべての変更は意識的であり、データは「漏れ」たり、暗黙に変更されたりしませんでした。
利点:
欠点: