どれくらい滞在するか分からないまま、旅行の準備をしようとしたことがありますか? Go にデータを保存すると、まさにそれが起こります。週末の旅行の荷造りなど、保管する必要がある物の数が正確にわかっている場合があります。また、旅行の荷物をまとめるときに「準備ができたら帰ります」と言いながら、そうしないこともあります。
Go 配列の世界を深く掘り下げ、簡単な図を通して内部構造を切り取ってみましょう。以下について検討します:
この記事を読み終わるまでに、実際の例とメモリ図を活用して、いつ配列を使用するのか、いつスライスを使用するのかを理解できるようになります。
配列は、完璧に配置されたボックスの行のように、各要素が隣り合った単一のメモリ ブロックであると考えてください。
変数番号 [5]int を宣言すると、Go は 5 つの整数を保持するのに十分な連続メモリを、それ以上でもそれ以下でも確保しません。
連続した固定メモリがあるため、実行時にサイズを変更することはできません。
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
サイズは配列の型の一部です。これは、int と string が異なるのと同様に、[5]int と [6]int は完全に異なる型であることを意味します。
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
Go で配列を割り当てたり渡したりすると、デフォルトでコピーが作成されます。これにより、データの分離が確保され、予期しない突然変異が防止されます。
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
それでは、vardynamic [size]int を実行して動的サイズを設定することはできません。ここで slice が登場します。
その魔法は、操作を高速に保ちながら、この柔軟性をどのように維持するかにあります。
Go のすべてのスライスは 3 つの重要なコンポーネントで構成されます。
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
何が危険ですか??
unsafe.Pointer は、型安全性の制約なしで生のメモリ アドレスを処理する Go の方法です。これは Go の型システムをバイパスし、直接メモリ操作を可能にするため、「安全ではない」 です。
これは、Go の C の void ポインターに相当すると考えてください。
その配列は何ですか?
スライスを作成すると、Go はバッキング配列と呼ばれるヒープ内に (配列とは異なり) 連続したメモリ ブロックを割り当てます。これで、スライス構造体の配列はそのメモリ ブロックの先頭を指します。
配列フィールドは次の理由により unsafe.Pointer を使用します:
実際のアルゴリズムの内部での直感を養ってみましょう。
直感に従っていくと、次の 2 つのことができます:
非常に広いスペースを確保でき、必要なときに必要に応じて使用できます
長所: 一定の時点まで増大するニーズに対応
短所: メモリの浪費、実質的に制限に達する可能性があります
最初にランダムなサイズを設定し、要素が追加されるたびに追加ごとにメモリを再割り当てできます
長所: 前のケースを処理し、必要に応じて拡張可能
短所: 再割り当てはコストが高く、追加するたびに最悪の状態になります
容量が限界に達した場合は拡張する必要があるため、再割り当てを避けることはできません。後続の挿入/追加コストが一定 (O(1)) になるように、再割り当てを最小限に抑えることができます。これを償却原価といいます。
どうすればよいでしょうか?
Go バージョン v1.17 までは、次の式が使用されていました:
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
Go バージョン v1.18 より:
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
大きなスライスを 2 倍にするのはメモリの無駄であるため、スライス サイズが増加すると成長係数は減少します。
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
スライスにいくつかの要素を追加しましょう
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
定員に達しましたので(5)>長さ (3)、進む:
既存のバッキング配列を使用します
10 をインデックス 3 に配置します
長さを 1 増やします
// Old growth pattern capacity = oldCapacity * 2 // Simple doubling
限界に挑戦しましょう
// New growth pattern if capacity < 256 { capacity = capacity * 2 } else { capacity = capacity + capacity/4 // 25% growth }
おっと!現在、私たちはキャパシティーに達しており、成長する必要があります。何が起こるかは次のとおりです:
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
大きなスライスの場合はどうなりますか?
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
容量は 256 であるため、Go は 1.18 以降の成長式を使用します。
新しい容量 = oldCap oldCap/4
256 256/4 = 256 64 = 320
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
スライスヘッダーは次のようになります:
// Old growth pattern capacity = oldCapacity * 2 // Simple doubling
誤った更新
スライスは参照セマンティクスを使用するため、注意しないと元のスライスへの偶発的な突然変異につながる可能性のあるコピーを作成しません。
// New growth pattern if capacity < 256 { capacity = capacity * 2 } else { capacity = capacity + capacity/4 // 25% growth }
高価な追加操作
numbers := make([]int, 3, 5) // length=3 capacity // Memory Layout after creation: Slice Header: { array: 0xc0000b2000 // Example memory address len: 3 cap: 5 } Backing Array at 0xc0000b2000: [0|0|0|unused|unused]
コピーと追加
numbers = append(numbers, 10)
明確な選択ガイドでこれを締めくくりましょう:
?次の場合に配列を選択します:
?次の場合にスライスを選択します:
? Notion-to-MD プロジェクトをチェックしてください。これは、Notion ページを Markdown に変換するツールで、コンテンツ作成者や開発者に最適です。 Discord コミュニティに参加してください。
以上がGo における配列とスライス: 「内部」機能を視覚的に理解するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。