【Go记录】go slice详解

本文详细介绍了Go语言中的slice结构,包括其依赖数组的特性,slice的创建、初始化、获取元素的方法,以及nil slice和空slice的区别。通过示例展示了slice的切片、copy、append操作,以及如何避免内存浪费。文章还讨论了slice在函数传递中的行为,以及如何处理可能的内存问题。
摘要由CSDN通过智能技术生成

本篇go version 1.18

Go slice

slice的存储结构

Go中的slice依赖于数组,具备数组所有的优点。
slice结构:先创建一个有特定长度和数据类型的底层数组,然后从这个底层数组中选取一部分元素,返回这些元素组成的集合,并将slice指向集合中的第一个元素。换句话说,slice自身维护了一个指针属性,指向它底层数组中的某些元素的集合。
例如,初始化一个slice数据结构:

func main() {
	test := make([]int, 3, 6)
	fmt.Println(test)
}

上述程序输出:

[0 0 0]

过程解析:

  1. 声明长度为6,数据类型为int的数组
  2. 取前3个元素,取index0-2元素
  3. slice struct指针指向底层数组的第一个元素,此处指向index为0的元素地址

底层数据结构:

源码路径:src/runtime/slice.go

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

slice struct中有3个成员:

  1. array:底层数组指针,指向底层分配的数组开始的元素地址;
  2. len:slice当前的长度;
  3. cap:底层数组的长度;

创建、初始化、获取元素

创建 、初始化

创建slice数据结构的方式
1:make()

	// 创建len和cap都为6的slice
	s1 := make([]int, 6)
	fmt.Println(s1)
	// 创建len为3、cap为6的slice
	s2 := make([]int, 3, 6)
	fmt.Println(s2)

make关键字:可以为底层数组分配内存,并从底层数组中生成一个slice并初始化。
make可以构建slice、map、channel三种数据对象,先为底层数据结构分配内存并初始化。

2: 直接赋值初始化

// 创建长度和容量为4的切片,并初始化
	s3 := []int{1, 2, 3, 4, 5, 6}
	fmt.Println(s3)

获取slice中元素

slice只能访问len范围内的元素,而在len之外,cap之内的元素不能被访问,panic如下代码段

	// 创建len为3、cap为6的slice,获取第4个元素
	s2 := make([]int, 3, 6)
	fmt.Println(s2[2]) // 0
	fmt.Println(s2[3]) // panic: runtime error: index out of range [3] with length 3

nil slice和Empty slice

nil slice

只声明一个slice,不做初始化时,此时是nil slice,不会指向任何数组,此时len和ca p都为0

// 声明一个nil slice
var nils []int
type slice struct {
	array unsafe.Pointer            // nil
	len   int						// 0
	cap   int						// 0 
}

empty slice

空slice是指底层数组指向的是长度为0的空数组。

// 声明一个空slice
emptys1 := make([]int,0)
emptys2 := []int{}
type slice struct {
	array unsafe.Pointer            // 地址
	len   int						// 0
	cap   int						// 0 
}

比较

虽然nil slice和Empty slice的长度和容量都为0,输出时的结果都是[],且都不存储任何数据,但它们是不同的。nil slice不会指向底层数组,而空slice会指向底层数组,只不过这个底层数组暂时是空数组。

	var nils []int
	emptys := []int{}
	println(nils) // [0/0]0x0
	println(emptys) // [0/0]0x14000062ef8

但是,无论是nil slice还是empty slice,都可以对它们进行操作,如append()函数、len()函数和cap()函数。

	fmt.Println(len(nils))    // 0
	fmt.Println(len(emptys))  // 0
	fmt.Println(cap(nils))    // 0
	fmt.Println(cap(emptys))  // 0
	nils = append(nils, 1)
	emptys = append(emptys, 1)
	fmt.Println(nils)         // [1]
	fmt.Println(emptys)       // [1]

切片

切片语法:

SLICE[A:B]
SLICE[A:B:C]

其中A表示从SLICE的第几个元素开始切,B控制切片的长度(B-A),C控制切片的容量(C-A),如果没有给定C,则表示切到底层数组的最尾部。

SLICE[A:]  // 从A切到最尾部
SLICE[:B]  // 从最开头切到B(不包含B)
SLICE[:]   // 从头切到尾,等价于复制整个SLICE

注意:截取时“左闭右开”

多个slice可能共享同一个底层数组,所以当修改了某个slice中的元素时,其他包含该元素的slice也会随之改变。

copy()函数

copy()可以将一个slice copy到另一个slice中。

$ go doc builtin copy
func copy(dst, src []Type) int

表示将src slice拷贝到dst slice,src比dst长,就截断,src比dst短,则只拷贝src那部分。
copy的返回值是拷贝成功的元素数量,所以也就是src slice或dst slice中最小的那个长度。

此外,copy还能将字符串拷贝到byte slice中,因为字符串实际上就是[]byte。

append()函数

当slice的length已经等于capacity的时候,再使用append()给slice追加元素,会触发扩容操作。
slice扩容操作,底层会生成一个新的数组,并将原来的底层数组的元素拷贝至新数组中,并返回新的slice。
换句话说,旧的底层数组还会被旧的slice引用,因此此时,新slice和旧slice不再共享同一个底层数组。
如下代码段:

var arr = []int{}
	for i := 0; i < 10; i++ {
		arr = append(arr, i)
		println(arr)
	}
	fmt.Println(arr)

输出结果:

[1/1]0x14000120008
[2/2]0x14000120010
[3/4]0x14000122000
[4/4]0x14000122000
[5/8]0x14000124000
[6/8]0x14000124000
[7/8]0x14000124000
[8/8]0x14000124000
[9/16]0x14000126000
[10/16]0x14000126000
[0 1 2 3 4 5 6 7 8 9]

上面说slice扩容时底层会生成一个新的数组,因此每次append触发扩容后,都会生成一个新的底层数组。

合并slice

append(s1,s2…)表示将s2合并在s1的后面,并返回新的slice。

注意:append()最多允许两个参数,所以一次性只能合并两个slice。但可以取巧,将append()作为另一个append()的参数,从而实现多级合并。例如,下面的合并s1和s2,然后再和s3合并,得到s1+s2+s3合并后的结果。

sn := append(append(s1,s2...),s3...)

切片/copy()/append()/合并slice

slice的操作(切片/copy()/append()/合并slice)都会产生新的slice,但新老slice在扩容时刻是共用底层数组的。
未触发扩容操作:新老slice增删改元素都会影响到原slice底层数组;
触发扩容操作:底层数组扩容会生成一个新的底层数组并且被新的slice指向,此时和老的slice完全没有关系,老的slice增删改元素完全不会影响新的slice。

slice遍历

range关键字可以对slice进行迭代,每次返回一个index和对应的元素值。可以将range的迭代结合for循环对slice进行遍历。

	s1 := []int{1,2,3,4,5,6}
    for index,value := range s1 {
        println("index:",index," , ","value",value)
    }

传递slice给函数

slice实际上包含了3个属性,它的数据结构类似于[3/5]0xc42003df10。
Go中函数的参数是按值传递的,所以调用函数时会复制一个参数的副本传递给函数,如果传递的是slice,它将复制该slice副本给函数,这个副本其实就是[3/5]0xc42003df10,所以传递给函数的副本仍然指向源slice的底层数组。
因此,如果函数内部对slice进行了修改,有可能会直接影响函数外部的底层数组,从而影响其他slice。但并不完全是这样,例如函数内部对slice进行扩容,扩容时生成了一个新的底层数组,函数后续的代码支队新的底层数组操作,这样就不会影响原始的底层数组。
未触发扩容:

func main() {
	var s = make([]int, 2, 4)
	s[0] = 1
	println(s)
	fmt.Println("pre method", s)
	testSliceNoExpand(s)
	println(s)
	fmt.Println("post method", s)
}
func testSliceNoExpand(s []int) {
	s = append(s, 3)
	println(s)
}

输出:

[2/4]0x140000aa000
pre method [1 0]
[4/4]0x140000aa000
in method [1 0 3 3]
[2/4]0x140000aa000
post method [1 0]

触发扩容:

func main() {
	var s = make([]int, 2, 4)
	s[0] = 1
	println(s)
	fmt.Println("pre method", s)
	testSliceExpand(s)
	println(s)
	fmt.Println("post method", s)
}
func testSliceExpand(s []int) {
	s = append(s, 3)
	s = append(s, 4)
	s = append(s, 5)
	println(s)
	fmt.Println("in method", s)
}

输出:

[2/4]0x1400012c000
pre method [1 0]
[5/8]0x14000130000
in method [1 0 3 4 5]
[2/4]0x1400012c000
post method [1 0]

slice和内存浪费问题

由于slice的底层是数组,很可能数组很大,但slice所取的元素数量却很小,这就导致数组占用的绝大多数空间是被浪费的。

特别地,垃圾回收器(GC)不会回收正在被引用的对象,当一个函数直接返回指向底层数组的slice时,这个底层数组将不会随函数退出而被回收,而是因为slice的引用而永远保留,除非返回的slice也消失。

因此,当函数的返回值是一个指向底层数组的数据结构时(如slice),应当在函数内部将slice拷贝一份保存到一个使用自己底层数组的新slice中,并返回这个新的slice。这样函数一退出,原来那个体积较大的底层数组就会被回收,保留在内存中的是小的slice。

本篇参考:Go基础系列:Go slice详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dnice

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值