Go最新Golang 中的 slice 详解_golang slice,【工作经验分享

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。

(2)数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不能存储任何数据,都是底层数组存储数据,修改切片的时候修改的是底层数组中的数据,切片一旦扩容,会指向一个新的底层数组,内存地址也就随之改变。

二、切片(slice)的底层实现

源码位于 src\runtime\slice.go 中。
  golang 中 slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。

type slice struct {
	array unsafe.Pointer // 指向底层数组的指针
	len   int // 长度 
	cap   int // 容量
}

注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。

看个经典的例子:

package main
import "fmt"
func main() {
	slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := slice[2:5]
	fmt.Println(s1) // [2 3 4]
	s2 := s1[2:6:7] // 长度 2:6,容量 2:7
	fmt.Println(s2) // [4 5 6 7]
	s2 = append(s2, 100)
	s2 = append(s2, 200)
	s1[2] = 20
	fmt.Println(s1)    // [2 3 20]
	fmt.Println(s2)    // [4 5 6 7 100 200]
	fmt.Println(slice) // [0 1 2 3 20 5 6 7 100 9]
}

s1 从 slice 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。s2 从 s1 的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。

在这里插入图片描述

接着,向 s2 尾部追加一个元素 100,s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。

在这里插入图片描述

再次向 s2 追加元素200,这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。这一改动,不会影响数组和 s1 。

在这里插入图片描述

最后,修改 s1 索引为2位置的元素,这次只会影响原始数组相应位置的元素,它影响不到 s2 了,人家已经远走高飞了。

在这里插入图片描述
  再提一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。

当 slice 作为函数参数时,是值传递,函数内部对 slice 的作用并不会改变外层的 slice ,要想真的改变外层 slice,只有将返回的新的 slice 赋值到原始 slice,或者向函数传递一个指向 slice 的指针。slice 结构体自身不会被改变,指针指向的底层数组的地址也不会被改变,改变的是数组中的数据。

package main
func main() {
    s := []int{1, 1, 1}
    f(s)
    fmt.Println(s) // [2 2 2]
}
func f(s []int) {
    // i只是一个副本,不能改变s中元素的值
    /\*for \_, i := range s {
 i++
 }
 \*/
    for i := range s {
        s[i] += 1
    }
}

三、切片(slice)的扩容

slice可以理解为动态数组,既然是动态数组,那必然需要进行扩容。

1、触发扩容的时机

向 slice 追加元素,如果底层数组的容量不够(即便底层数组并未填满),就会触发扩容。追加元素调用的是 append 函数。

2、扩容规则

Go <= 1.17

1、首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。

2、否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的 2 倍

3、否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)就是旧容量(old.cap)按照 1.25 倍循环递增,也就是每次加上 cap / 4。

4、如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

Go1.18之后

引入了新的扩容规则,首先 1024 的边界不复存在,取而代之的常量是 256 。超出256的情况,也不是直接扩容25%,而是设计了一个平滑过渡的计算方法,随着容量增大,扩容比例逐渐从100%平滑降低到25%,从 2 倍平滑过渡到 1.25 倍

为什么要这样设计?

避免追加过程中频繁扩容,减少内存分配和数据复制开销,有助于性能提升。

3、内存对齐

计算出了新容量之后,还没有完,出于内存的高效利用考虑,还要进行内存对齐。进行内存对齐之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。

4、完整过程

向 slice 追加元素的时候,若容量不够,会触发扩容,会调用 growslice() 函数。首先,根据扩容规则,计算出新的容量,然后进行内存对齐,之后,向 Go 内存管理器申请内存,将老 slice 中的数据整个复制过去,并且将追加的元素添加到新的底层数组中。

四、切片(slice)的拷贝

1、浅拷贝

浅拷贝,拷贝的是地址,浅拷贝只复制了指向底层数据结构的指针,而不是复制整个底层数据结构,修改修改新对象的值会影响原对象值。对于引用类型,如切片和字典等都是浅拷贝。

slice2 := slice1

arr := []int{1, 2, 3}
copyArr := arr // 浅拷贝
copyArr[0] = 100 // 修改变量
fmt.Println(arr) // [100 2 3],原始变量的值也随之改变

slice1和slice2指向的都是同一个底层数组,任何一个数组元素被改变,都可能会影响两个slice。在slice触发扩容操作前,slice1和slice2指向的都是相同数组,但在触发扩容操作后,二者指向的就不一定是相同的底层数组了。

在Go中,切片和字典类型默认都是以引用方式传递,所以默认情况下进行的是浅拷贝,如果需要进行深拷贝,则需要自己实现。对于值类型(如基本数据类型和结构体),都是以值拷贝的方式进行的,即进行深拷贝。

2、深拷贝

深拷贝,拷贝的是数据本身,完全复制了底层数据结构,而不是复制指向底层数据结构的指针,会创建一个新对象,新对象和原对象不共享内存,它们是完全独立的,修改新对象的值不会影响原对象值,内存地址不同,释放内存地址时,可以分别释放。

copy(slice2, slice1)  

src := []int{1, 2, 3, 4, 5}
dst := make([]int, len(src), cap(src))
copy(dst, src) // 深拷贝
dst[0] = 100 // 修改变量
fmt.Println(src) // [1 2 3 4 5]
fmt.Println(dst) // [100 2 3 4 5]

short := make([]int, 3, 3)
copy(short, src)   // 深拷贝
fmt.Println(short) // [1 2 3]

long := make([]int, 10, 10)
copy(long, src)   // 深拷贝
fmt.Println(long) // [1 2 3 4 5 0 0 0 0 0]

把 slice1 的数据复制到 slice2 中,修改 slice2 的数据,不会影响到 slice1 。如果 slice2 的长度和容量小于 slice1 的,那么只会复制 slice2 长度的数据。

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值