上一篇介绍了slice的声明方式,这一篇介绍切片的使用方式

切片容量的追加

在使用make的时候不仅可以制定切片的长度,还可以制定切片的容量。

例如:

    var numbers = make([]int,3 ,5)

    fmt.Printf("长度 = %d, 容量= %d, slice = %v\n", len(numbers), cap(numbers), numbers) // cap()函数为取出切片的容量

用下图来展示一下make分配的内存空间内部的结构(这里的内存空间在每次扩容时会重新分配,不会是原本的内存地址)

  • 使用完make之后,numbers是指向切片的头部的一个首指针
  • ptr为指向切片尾部的指针,指向切片合法元素的最后一个位置
  • ptr之后的是容量预留的空间长度(预留的为5个长度,但是已经使用了3个)

QQ图片20210801181756.png
使用append向数组中追加元素:

    var numbers = make([]int,3 ,5)

    fmt.Printf("长度 = %d, 容量= %d, slice = %v\n", len(numbers), cap(numbers), numbers) // cap()函数为取出切片的容量

    //向numbers切片追加一个元素1,numbers的长度变为4,numbers 的值为[0,0,0,1],但是容量依旧为5
    numbers = append(numbers, 1)
    
    fmt.Printf("长度 = %d, 容量= %d, slice = %v\n", len(numbers), cap(numbers), numbers)

运行结果:
image.png

如果已经使用的长度已经到达容量预留的最大值,再使用append追加时,内存空间不够,golang会重新分配内存空间,地址发生变化,容量进行扩展,并不是在原有基础上扩展(下面为求证实验)

golang的append扩展内存机制研究

扩展的内存大小研究

验证方式:每次扩容时比较容量和长度的差
使用以下代码来检测每次自动扩展时扩展的内存空间的大小:

    a :=  make([]int, 0)
    n := 65536
    nowCap := 0
    for i := 0; i < n; i++ {
        a = append(a, 1)
        if nowCap != cap(a) {
            nowCap = cap(a)
            fmt.Printf("len=%d cap=%d 扩大了:%d\n", len(a), cap(a), cap(a)-len(a)+1)
        }
    }

运行结果:

len=1 cap=1 扩大了:1
len=2 cap=2 扩大了:1
len=3 cap=4 扩大了:2
len=5 cap=8 扩大了:4
len=9 cap=16 扩大了:8
len=17 cap=32 扩大了:16
len=33 cap=64 扩大了:32
len=65 cap=128 扩大了:64
len=129 cap=256 扩大了:128
len=257 cap=512 扩大了:256
len=513 cap=1024 扩大了:512
len=1025 cap=1280 扩大了:256
len=1281 cap=1696 扩大了:416
len=1697 cap=2304 扩大了:608
len=2305 cap=3072 扩大了:768
len=3073 cap=4096 扩大了:1024
len=4097 cap=5120 扩大了:1024
len=5121 cap=7168 扩大了:2048
len=7169 cap=9216 扩大了:2048
len=9217 cap=12288 扩大了:3072
len=12289 cap=15360 扩大了:3072
len=15361 cap=19456 扩大了:4096
len=19457 cap=24576 扩大了:5120
len=24577 cap=30720 扩大了:6144
len=30721 cap=38912 扩大了:8192
len=38913 cap=49152 扩大了:10240
len=49153 cap=61440 扩大了:12288
len=61441 cap=76800 扩大了:15360

Process finished with exit code 0

可以看到扩展时的规则为:当容量(cap)小于1000时,扩展时扩展为之前的两倍,当容量大于1000时,扩展时扩展为之前的1.25倍。

验证扩展时是否完全重新分配内存空间

研究append时,内存扩展时是不是在原有基础上扩展,还是完全重新开辟了新的地址。

验证方式:每次扩容时打印内存地址。

代码如下:

    a :=  make([]int, 1)
    fmt.Printf("a切片的内存地址 = %p\n", a)
    fmt.Printf("a[0]内存地址 = %p\n", &a[0])
    a = append(a, 1) // 向a切片追加一个元素为1
    fmt.Println("==========自动扩展内存=================")
    fmt.Printf("a切片的内存地址 = %p\n", a)
    fmt.Printf("a[0]内存地址 = %p\n", &a[0])
    fmt.Printf("a[1]内存地址 = %p\n", &a[1])

运行结果:
image.png
可以发现扩容前和扩容后内存地址不一样,所以是重新分配内存地址。

切片的长度len和容量cap

切片的截取

截取和python的数组切片基本一致,按照左闭右开来取出切片。

部分截取

    s := []int{1,2,3}
    s1:= s[0:2] // 按照左取右不取的原则取出第0和第1个元素即[1,2]
    fmt.Println(s1)

运行结果:
符合左闭右开的截取规则
image.png
注意:s和s1的数组指向的地址是相同的,所以修改s1中的元素,s切片中的值也会响应的修改.
例如:

    s := []int{1,2,3}
    s1:= s[0:2] // 按照左取右不取的原则取出第0和第1个元素即[1,2]
    fmt.Println(s1)
    s1[0] = 100
    fmt.Println(s)
    fmt.Println(s1)

运行结果:
image.png
结论就是,当把截取之后的切片直接赋值给新的一个切片,新的切片指向的地址和被截取的切片的地址是相同的,也就是浅拷贝。

数组的深拷贝

使用copy(新切片, 被拷贝的切片)来完成深拷贝,这样个切片的空间是独立分开的。
例如:

    s := []int{1,2,3}
    s2 := make([]int, 3) // s2 = [0,0,0]
    copy(s2,s) // 将s的值深拷贝到s2中
    fmt.Printf("s2 = %d\n", s2)
    fmt.Printf("s的内存地址 = %p\n", &s)
    fmt.Printf("s2的内存地址 = %p\n", &s2)

运行结果:
image.png
可以看到深拷贝的内存地址是不同的。

切片的删除

Golang 切片删除指定元素的几种方法

最后修改:2024 年 03 月 13 日
如果觉得我的文章对你有用,请随意赞赏