您是否曾经尝试过收拾行李去旅行而不知道要在那里待多久?这正是我们在 Go 中存储数据时发生的情况。有时,比如周末旅行收拾行李时,我们确切地知道需要存放多少东西;其他时候,例如,在收拾行李准备旅行时,我们会说“我准备好后就回来”,但我们不会。
让我们深入了解 Go 数组的世界,并通过简单的插图剖析内部结构。我们将调查:
读完本文后,您将能够借助现实示例和内存图了解何时使用数组以及何时使用切片
将数组视为单个内存块,其中每个元素彼此相邻,就像一排完美排列的盒子。
当你声明 var number [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 }
大小是数组类型的一部分。这意味着 [5]int 和 [6]int 是完全不同的类型,就像 int 和 string 不同一样。
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 中的每个切片都包含三个关键组件:
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 因为:
让我们尝试培养对底层实际算法的直觉。
如果我们遵循直觉我们可以做两件事:
我们可以留出这么大的空间,需要的时候就可以使用
优点:在一定程度上满足不断增长的需求
缺点:内存浪费,实际上可能会达到极限
我们可以最初设置一个随机大小,当元素被附加时,我们可以在每个附加上重新分配内存
优点:处理之前的情况,可以根据需要进行增长
缺点:重新分配的成本很高,而且每次追加都会变得最糟糕
我们无法避免重新分配,因为当容量达到需要增长时。我们可以最小化重新分配,以便后续插入/追加成本保持不变 (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 }
由于将大切片加倍会浪费内存,因此随着切片大小的增加,生长因子会减少。
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 的工具,非常适合内容创建者和开发人员。加入我们的不和谐社区。
以上是Go 中的数组与切片:以视觉方式理解'底层”功能的详细内容。更多信息请关注PHP中文网其他相关文章!