ホームページ > バックエンド開発 > Golang > Go における配列とスライス: 「内部」機能を視覚的に理解する

Go における配列とスライス: 「内部」機能を視覚的に理解する

Patricia Arquette
リリース: 2024-12-21 18:27:15
オリジナル
275 人が閲覧しました

Arrays vs Slices in Go: Understanding the

どれくらい滞在するか分からないまま、旅行の準備をしようとしたことがありますか? Go にデータを保存すると、まさにそれが起こります。週末の旅行の荷造りなど、保管する必要がある物の数が正確にわかっている場合があります。また、旅行の荷物をまとめるときに「準備ができたら帰ります」と言いながら、そうしないこともあります。

Go 配列の世界を深く掘り下げ、簡単な図を通して内部構造を切り取ってみましょう。以下について検討します:

  1. メモリレイアウト
  2. 成長メカニズム
  3. 参照セマンティクス
  4. パフォーマンスへの影響

この記事を読み終わるまでに、実際の例とメモリ図を活用して、いつ配列を使用するのか、いつスライスを使用するのかを理解できるようになります。

配列: 固定サイズのコンテナ ?

配列は、完璧に配置されたボックスの行のように、各要素が隣り合った単一のメモリ ブロックであると考えてください。

変数番号 [5]int を宣言すると、Go は 5 つの整数を保持するのに十分な連続メモリを、それ以上でもそれ以下でも確保しません。

Arrays vs Slices in Go: Understanding the

連続した固定メモリがあるため、実行時にサイズを変更することはできません。

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
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

Arrays vs Slices in Go: Understanding the

サイズは配列の型の一部です。これは、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 で配列を割り当てたり渡したりすると、デフォルトでコピーが作成されます。これにより、データの分離が確保され、予期しない突然変異が防止されます。

Arrays vs Slices in Go: Understanding the

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 つの重要なコンポーネントで構成されます。

Arrays vs Slices in Go: Understanding the

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 を使用します:

  1. 型情報なしで生のメモリを指す必要があります
  2. これにより、型ごとに個別のコードを生成することなく、Go が任意の型 T のスライスを実装できるようになります。

スライスの動的メカニズム

実際のアルゴリズムの内部での直感を養ってみましょう。

Arrays vs Slices in Go: Understanding the

直感に従っていくと、次の 2 つのことができます:

  1. 非常に広いスペースを確保でき、必要なときに必要に応じて使用できます
    長所: 一定の時点まで増大するニーズに対応
    短所: メモリの浪費、実質的に制限に達する可能性があります

  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 倍にするのはメモリの無駄であるため、スライス サイズが増加すると成長係数は減少します。

使い方の観点から理解を深めましょう

Arrays vs Slices in Go: Understanding the

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
}
ログイン後にコピー
ログイン後にコピー

おっと!現在、私たちはキャパシティーに達しており、成長する必要があります。何が起こるかは次のとおりです:

  1. 新しい容量を計算します (oldCap
  2. 新しいバッキング配列 (新しいメモリ アドレス、たとえば 300) を割り当てます
  3. 既存の要素を新しいバッキング配列にコピーします
  4. 新しい要素を追加します
  5. スライスヘッダーを更新します

Arrays vs Slices in Go: Understanding the

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
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

なぜセマンティクスを参照するのでしょうか?

  1. パフォーマンス: 大規模なデータ構造のコピーにはコストがかかります
  2. メモリ効率: 不必要なデータの重複を回避します
  3. データの共有ビューの有効化: 複数のスライスが同じバッキング配列を参照できる
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)
ログイン後にコピー

Arrays vs Slices in Go: Understanding the

明確な選択ガイドでこれを締めくくりましょう:

?次の場合に配列を選択します:

  1. 正確なサイズは事前にわかります
  2. 小さな固定データ (座標、RGB 値など) の操作
  3. パフォーマンスは重要であり、データはスタックに収まります
  4. サイズに応じたタイプ セーフティが必要です

?次の場合にスライスを選択します:

  1. サイズは変更される可能性があります
  2. 動的データの操作
  3. 同じデータの複数のビューが必要
  4. ストリーム/コレクションの処理

? Notion-to-MD プロジェクトをチェックしてください。これは、Notion ページを Markdown に変換するツールで、コンテンツ作成者や開発者に最適です。 Discord コミュニティに参加してください。

以上がGo における配列とスライス: 「内部」機能を視覚的に理解するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート