這是帖子的摘錄;完整的文章可以在這裡找到:Go 數組如何工作以及如何使用 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 位元架構上,int 大小為 8 個位元組。然後我們使用這些指標來存取並將它們轉換回 int 值。
這個範例只是為了教育目的而使用 unsafe 套件直接存取記憶體。在不了解後果的情況下,不要在生產中這樣做。
現在,類型 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 會產生指令將值逐一放入陣列中。
所以當我們執行 arr := [3]int{1, 2, 3, 4} 時,實際發生的是:
arr := [4]int{} arr[0] = 1 arr[1] = 2 arr[2] = 3 arr[3] = 4
這種策略稱為本地程式碼初始化。這意味著初始化程式碼是在特定函數的範圍內產生和執行的,而不是全域或靜態初始化程式碼的一部分。
當你閱讀下面的另一個初始化策略時,你會變得更清楚,其中的值並不是像那樣一個一個地放入數組中。
「超過 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.
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中文網其他相關文章!