ホームページ >バックエンド開発 >Golang >Go 配列の仕組みと For-Range の扱いが難しい

Go 配列の仕組みと For-Range の扱いが難しい

WBOY
WBOYオリジナル
2024-08-20 18:44:00338ブラウズ

How Go Arrays Work and Get Tricky with For-Range

Go 配列の仕組みと For-Range の扱いが難しい

これは投稿の抜粋です。投稿全文はこちらからご覧いただけます: How Go Arrays Work and Get Tricky with For-Range.

古典的な Golang の配列とスライスは非常に簡単です。配列は固定サイズであり、スライスは動的です。ただし、言っておきたいのは、Go は表面的には単純に見えるかもしれませんが、内部では多くのことが起こっているということです。

いつものように、基本から始めて、それからもう少し深く掘り下げていきます。心配しないでください。配列はさまざまな角度から見ると非常に興味深いものになります。

スライスについては次のパートで説明します。準備ができたらここにドロップします。

配列とは何ですか?

Go の配列は他のプログラミング言語の配列とよく似ています。サイズは固定されており、同じタイプの要素を連続したメモリ位置に保存します。

これは、アドレスが配列の開始アドレスと要素のインデックスに基づいて計算されるため、Go が各要素にすばやくアクセスできることを意味します。

func main() {
    arr := [5]byte{0, 1, 2, 3, 4}
    println("arr", &arr)

    for i := range arr {
        println(i, &arr[i])
    }
}

// Output:
// arr 0x1400005072b
// 0 0x1400005072b
// 1 0x1400005072c
// 2 0x1400005072d
// 3 0x1400005072e
// 4 0x1400005072f

ここで注意すべき点がいくつかあります:

  • 配列 arr のアドレスは、最初の要素のアドレスと同じです。
  • 要素タイプがバイトであるため、各要素のアドレスは互いに 1 バイト離れています。

How Go Arrays Work and Get Tricky with For-Range

メモリ内の配列 [5] バイト{0, 1, 2, 3, 4}

画像をよく見てください。

私たちのスタックは、上位のアドレスから下位のアドレスへと下向きに成長していますよね?この図は、arr[4] から arr[0] まで、スタック内で配列がどのように見えるかを正確に示しています。

ということは、最初の要素 (または配列) のアドレスと要素のサイズがわかれば、配列の任意の要素にアクセスできるということですか? int 配列と安全でないパッケージを使用してこれを試してみましょう:

func main() {
    a := [3]int{99, 100, 101}

    p := unsafe.Pointer(&a[0])

    a1 := unsafe.Pointer(uintptr(p) + 8)
    a2 := unsafe.Pointer(uintptr(p) + 16)

    fmt.Println(*(*int)(p))
    fmt.Println(*(*int)(a1))
    fmt.Println(*(*int)(a2))
}

// Output:
// 99
// 100
// 101

最初の要素へのポインタを取得し、int のサイズ (64 ビット アーキテクチャでは 8 バイト) の倍数を加算して次の要素へのポインタを計算します。次に、これらのポインターを使用してアクセスし、int 値に変換します。

How Go Arrays Work and Get Tricky with For-Range

メモリ内の配列 [3]int{99, 100, 101}

この例は、教育目的でメモリに直接アクセスするための安全でないパッケージを試してみたものです。結果を理解せずに運用環境でこれを実行しないでください。

型 T の配列それ自体は型ではありませんが、特定のサイズと型 T を持つ配列は型とみなされます。私が言いたいのは次のとおりです:

func main() {
    a := [5]byte{}
    b := [4]byte{}

    fmt.Printf("%T\n", a) // [5]uint8
    fmt.Printf("%T\n", b) // [4]uint8

    // cannot use b (variable of type [4]byte) as [5]byte value in assignment
    a = b 
}

a と b は両方ともバイトの配列ですが、Go コンパイラーはそれらを完全に異なる型として認識します。%T 形式によりこの点が明確になります。

Go コンパイラーが内部でこれをどのように認識するかは次のとおりです (src/cmd/compile/internal/types2/array.go):

// An Array represents an array type.
type Array struct {
    len  int64
    elem Type
}

// NewArray returns a new array type for the given element type and length.
// A negative length indicates an unknown length.
func NewArray(elem Type, len int64) *Array { return &Array{len: len, elem: elem} }

配列の長さは型自体で「エンコード」されるため、コンパイラーは配列の長さをその型から認識します。あるサイズの配列を別のサイズに割り当てたり、比較しようとすると、型の不一致エラーが発生します。

配列リテラル

Go では配列を初期化する方法がたくさんありますが、そのうちのいくつかは実際のプロジェクトではめったに使用されない可能性があります。

var arr1 [10]int // [0 0 0 0 0 0 0 0 0 0]

// With value, infer-length
arr2 := [...]int{1, 2, 3, 4, 5} // [1 2 3 4 5]

// With index, infer-length
arr3 := [...]int{11: 3} // [0 0 0 0 0 0 0 0 0 0 0 3]

// Combined index and value
arr4 := [5]int{1, 4: 5} // [1 0 0 0 5]
arr5 := [5]int{2: 3, 4, 4: 5} // [0 0 3 4 5]

上で行っていること (最初のものを除く) は、値の定義と初期化の両方であり、これは「複合リテラル」と呼ばれます。この用語は、スライス、マップ、構造体にも使用されます。

ここで興味深いことに、要素が 4 つ未満の配列を作成すると、Go は値を 1 つずつ配列に入れる命令を生成します。

arr := [3]int{1, 2, 3, 4} を実行すると、実際に何が起こっているかは次のとおりです。

arr := [4]int{}
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4

この戦略はローカルコードの初期化と呼ばれます。これは、初期化コードがグローバルまたは静的な初期化コードの一部ではなく、特定の関数のスコープ内で生成および実行されることを意味します。

以下の別の初期化戦略を読むと、このように値が 1 つずつ配列に配置されるわけではないことがより明確になります。

「要素数が 4 つを超える配列はどうなりますか?」

コンパイラは、バイナリで配列の静的表現を作成します。これは、「静的初期化」戦略として知られています。

This means the values of the array elements are stored in a read-only section of the binary. This static data is created at compile time, so the values are directly embedded into the binary. If you're curious how [5]int{1,2,3,4,5} looks like in Go assembly:

main..stmp_1 SRODATA static size=40
    0x0000 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00  ................
    0x0010 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00  ................
    0x0020 05 00 00 00 00 00 00 00                          ........

It's not easy to see the value of the array, we can still get some key info from this.

Our data is stored in stmp_1, which is read-only static data with a size of 40 bytes (8 bytes for each element), and the address of this data is hardcoded in the binary.

The compiler generates code to reference this static data. When our application runs, it can directly use this pre-initialized data without needing additional code to set up the array.

const readonly = [5]int{1, 2, 3, 4, 5}

arr := readonly

"What about an array with 5 elements but only 3 of them initialized?"

Good question, this literal [5]int{1,2,3} falls into the first category, where Go puts the value into the array one by one.

While talking about defining and initializing arrays, we should mention that not every array is allocated on the stack. If it's too big, it gets moved to the heap.

But how big is "too big," you might ask.

As of Go 1.23, if the size of the variable, not just array, exceeds a constant value MaxStackVarSize, which is currently 10 MB, it will be considered too large for stack allocation and will escape to the heap.

func main() {
    a := [10 * 1024 * 1024]byte{}
    println(&a)

    b := [10*1024*1024 + 1]byte{}
    println(&b)
}

In this scenario, b will move to the heap while a won't.

Array operations

The length of the array is encoded in the type itself. Even though arrays don't have a cap property, we can still get it:

func main() {
    a := [5]int{1, 2, 3}
    println(len(a)) // 5
    println(cap(a)) // 5
}

The capacity equals the length, no doubt, but the most important thing is that we know this at compile time, right?

So len(a) doesn't make sense to the compiler because it's not a runtime property, Go compiler knows the value at compile time.

...

This is an excerpt of the post; the full post is available here: How Go Arrays Work and Get Tricky with For-Range.

以上がGo 配列の仕組みと For-Range の扱いが難しいの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。