首页 > 后端开发 > Golang > Go 中的数组与切片:以视觉方式理解'底层”功能

Go 中的数组与切片:以视觉方式理解'底层”功能

Patricia Arquette
发布: 2024-12-21 18:27:15
原创
277 人浏览过

Arrays vs Slices in Go: Understanding the

您是否曾经尝试过收拾行李去旅行而不知道要在那里待多久?这正是我们在 Go 中存储数据时发生的情况。有时,比如周末旅行收拾行李时,我们确切地知道需要存放多少东西;其他时候,例如,在收拾行李准备旅行时,我们会说“我准备好后就回来”,但我们不会。

让我们深入了解 Go 数组的世界,并通过简单的插图剖析内部结构。我们将调查:

  1. 内存布局
  2. 成长机制
  3. 参考语义
  4. 性能影响

读完本文后,您将能够借助现实示例和内存图了解何时使用数组以及何时使用切片

数组:固定大小的容器?

将数组视为单个内存块,其中每个元素彼此相邻,就像一排完美排列的盒子。

当你声明 var number [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

大小是数组类型的一部分。这意味着 [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 中分配或传递数组时,它们默认创建副本。这确保了数据隔离并防止意外突变。

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 中的每个切片都包含三个关键组件:

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

如果我们遵循直觉我们可以做两件事:

  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
}
登录后复制
登录后复制
登录后复制

由于将大切片加倍会浪费内存,因此随着切片大小的增加,生长因子会减少。

让我们从使用的角度更好地理解

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
登录后复制
登录后复制

slice的使用模式和注意事项

意外更新

由于切片使用引用语义,因此它不会创建副本,如果不注意,可能会导致原始切片意外突变。

// 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 的工具,非常适合内容创建者和开发人员。加入我们的不和谐社区。

以上是Go 中的数组与切片:以视觉方式理解'底层”功能的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板