これは投稿の抜粋です。投稿全文はこちらからご覧いただけます: 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[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 値に変換します。
この例は、教育目的でメモリに直接アクセスするための安全でないパッケージを試してみたものです。結果を理解せずに運用環境でこれを実行しないでください。
型 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 つを超える配列はどうなりますか?」
コンパイラは、バイナリ内の配列の静的表現を作成します。これは、「静的初期化」戦略として知られています。
これは、配列要素の値がバイナリの読み取り専用セクションに格納されることを意味します。この静的データはコンパイル時に作成されるため、値はバイナリに直接埋め込まれます。 Go アセンブリで [5]int{1,2,3,4,5} がどのように見えるか興味がある場合は、次のようにしてください:
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 ........
配列の値を確認するのは簡単ではありませんが、ここから重要な情報を得ることができます。
データは stmp_1 に保存されます。これは、サイズが 40 バイト (各要素あたり 8 バイト) の読み取り専用の静的データであり、このデータのアドレスはバイナリでハードコーディングされています。
コンパイラは、この静的データを参照するコードを生成します。アプリケーションを実行すると、配列を設定するための追加のコードを必要とせずに、この事前に初期化されたデータを直接使用できます。
const readonly = [5]int{1, 2, 3, 4, 5} arr := readonly
「要素が 5 つあるが、そのうちの 3 つだけが初期化されている配列はどうでしょうか?」
いい質問ですね。このリテラル [5]int{1,2,3} は最初のカテゴリに分類され、Go は値を 1 つずつ配列に入れます。
配列の定義と初期化について話しているときに、すべての配列がスタックに割り当てられるわけではないことに言及する必要があります。大きすぎる場合はヒープに移動されます。
しかし、どのくらいの大きさが「大きすぎる」のか疑問に思うかもしれません。
Go 1.23 では、配列だけでなく変数のサイズが定数値 MaxStackVarSize (現在 10 MB) を超えると、スタック割り当てには大きすぎるとみなされ、ヒープにエスケープされます。
func main() { a := [10 * 1024 * 1024]byte{} println(&a) b := [10*1024*1024 1]byte{} println(&b) }
このシナリオでは、b はヒープに移動しますが、a は移動しません。
配列の長さは型自体でエンコードされます。配列に cap プロパティがない場合でも、それを取得できます:
func main() { a := [5]int{1, 2, 3} println(len(a)) // 5 println(cap(a)) // 5 }
容量が長さに等しいのは間違いありませんが、最も重要なことはコンパイル時にこれがわかるということですよね?
したがって、len(a) は実行時プロパティではないため、コンパイラーにとって意味がありません。Go コンパイラーはコンパイル時に値を知っています。
...
これは投稿の抜粋です。投稿全文はこちらからご覧いただけます: How Go Arrays Work and Get Tricky with For-Range.
免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。
Copyright© 2022 湘ICP备2022001581号-3