打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
揭秘 Go 切片(Slice)的秘密

当向切片添加新参数时,底层数组会发生什么变化?它会扩展以容纳更多元素吗?

在这篇文章中,我们将深入探讨切片的内部工作原理,以及如何利用这些知识来进行更好的内存管理和性能优化。

具体而言,我们将探索 Go 中切片的底层实现和内存管理机制。

让我们开始吧!

查看数组

要深入了解切片的结构,必须仔细查看其底层类型:数组。

func main() {
    a := [5]int{}
    fmt.Printf("%p, %p\n", &a, &a[0])
}

// 0x14000018240, 0x14000018240

正如您可能已经了解的那样,数组中第一个元素的内存位置也是数组本身的内存位置(这意味着当您将数组传递给函数或赋值给变量时,您实际上是传递或赋值了第一个元素的内存地址)。

因此,数组的内存布局是连续的内存块,每个元素依次放置在相邻的位置上。

切片的结构

切片有三个主要组成部分:

  • · 底层数组指针:底层数组指针指向切片第一个元素的内存位置。

  • · 长度:当前在切片中被使用或可访问的元素数量。

  • · 容量:底层数组中可以存储的总元素数量,从底层数组指针开始计算。

这些关于切片结构的信息是从Go运行时库中获取的。现在,让我们更详细地了解一下。

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

出于演示目的,这里有一个关于切片长度和容量概念的示例(如果你已经熟悉这些概念,可以忽略这个示例)。

func main() {
    original := []int{01234}
    s := original[1:2
    fmt.Println(len(s), cap(s))
}

// 1 4

在这个示例中,我们可以看到切片s等于[]int{1},它的容量是从原始数组的索引1到索引4的部分计算得到的。

底层数组将会改变

需要注意的是,修改切片中的元素有时会影响到底层的数组,但并非总是如此,也不应该依赖这种行为。

在某些情况下,底层的数组可能会发生改变,导致切片也发生改变。然而,在编写代码时不应该依赖这种行为,因为它可能会导致意想不到的结果。

func main() {
    original := []int{01234}
    s := original[:]

    fmt.Println("Same array:")
    s[0] = 100
    fmt.Println(original, s)

    fmt.Println("Different array:")
    s = append(s, 5)
    s[0] = 200
    fmt.Println(original, s)
}

// Same array:
// [100 1 2 3 4] [100 1 2 3 4]
// Different array:
// [100 1 2 3 4] [200 1 2 3 4 5]

在实际情况中,append()函数不仅仅是用于添加元素。它还负责处理切片的分配和调整大小。

  1. 1. 它会检查切片是否有足够的容量来存储新的元素。

  2. 2. 如果容量不足,它会创建一个具有更大容量的新切片,复制原始切片的元素到新切片,并将新切片赋值给原始切片。

  3. 3. 然后,它将新的元素添加到切片中。

这是我用更简单的方式重写的append()函数版本,利用了泛型:

func append[T any](s []T, x ...T) []T {
    n := len(s)
    maxN := len(s) + len(x)

    // If there is not enough capacity, create a new slice with larger capacity
    if n+len(x) > cap(s) {
        newSlice := make([]T, maxN, maxN*2)
        copy(newSlice, s) // Copy the elements from the original slice to the new slice
        s = newSlice
    }

    s = s[:maxN]
    copy(s[n:], x)
    return s
}

预分配技术

重新调整切片大小在性能和内存方面可能非常昂贵,因为它需要分配一个新的切片并将所有元素复制过去。

这就是为什么在使用切片时,如果我们可以预测它们将保存的元素数量,通常最好进行预分配。这有助于提高性能并防止不必要的内存分配。

“是否可以同时使用“append()”和预分配?使用索引赋值可能很麻烦”

是的,这是可能的。

你可以使用make()函数进行预分配切片,传入两个变量,一个用于长度,另一个用于容量,而不是只传入一个。这可以消除索引赋值的需要。

func main() {
    s := make([]int, 0, 3)
    s = append2(s, 1, 2, 3, 4)
    fmt.Println(s)
}

下一次冒险

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Go 语言系列11:切片
Go 切片传递的隐藏危机
详解 Go 的切片
Go语言切片一网打尽,别和Java语法傻傻分不清
关于Go的Channel,Silce,Map
golang2021数据格式(30)切片容量是怎样增长
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服