领先的免费Web技术教程,涵盖HTML到ASP.NET

网站首页 > 知识剖析 正文

你说你熟悉Slice,这道slice题你能答对吗?

nixiaole 2024-11-10 12:31:07 知识剖析 15 ℃

每当你花费大量时间使用某种特定工具时,深入了解它并了解如何高效地使用它是很值得的。

最近,看到一个关于slice的面试题,感觉挺有意思。如下:

var test, another []uint8
fmt.Println("len1:", len(test), cap(test))

test = make([]uint8, 5, 10)               
fmt.Println("len2:", len(test), cap(test))

another = append(test, 1, 2, 3)           
fmt.Println("len3:", len(another), cap(another)) 

fmt.Println("len4:", len(test), cap(test)) 

copy(test, []uint8{6, 6, 6, 6})
fmt.Println("content1:", another, test) 

another = append(another, 1, 2, 3)
fmt.Println("len5:", len(another), cap(another))

copy(test, []uint8{5, 5})
fmt.Println("content2:", another[0], test[0])

这道面试题并不难,但是囊括了slice的方方面面。建议先停下来,仔细想下,并实际运行下,看看自己是否对slice完全了解了,没条件实际运行的读者可以看文末实际输出。

也可以在评论区写出你的答案,我们一起讨论。

slice到底是什么?和数组有什么关系?

slice是Go语言提供的重要数据类型,也是Gopher日常编码中最常使用的类型之一,在go语言规范中,有这么一句话:在使用过程中,应该尽量避免使用数组,使用slice替代数组。这是为什么呢?

在Go语言中,数组是一个固定长度的、存储同构类型元素的连续序列。根据描述,可以得知,数组有两个属性:类型、长度。这两个属性相同的数组是等价的。

比如下面的abc是三个不同的数组。其中,ab长度相同,但类型不同,ac类型相同,但长度不同。

var a [5]int
var b [6]byte
var c [6[int

在Go语言中传递数组是纯粹的值拷贝,对于元素类型长度较大或元素个数较多的数组,如果直接以数组类型参数传递到函数中会有不小的性能损耗。

根据Golang编码规范,推荐使用slice,其实,在Go语言中,数组充当的更多是“幕后使者”的角色,作为一种底层存储数据结构,而slice,则走到台前,作为底层数据结构的访问。

切片打开了访问底层数组的新姿势

从上图可以看出,操作slice实际上就是操作数组。

slice是引用类型,更适合用来做函数参数之间传递。这是因为slice作为函数参数传递的时候,实际传递的是slice的内部表示,带来的性能损耗很小且恒定,甚至是忽略不计。

slice在Golang中的内部表示如下:

type slice struct {
	array unsafe.Pointer // 指向底层数组的指针
	len   int            // 长度,即slice中当前元素的个数
	cap   int            // 容量
}

一般,我们这么创建slice:

s := make([]byte,5)

runtime下slice的表示

使用make创建slice的时候,如果没有制定cap的值,则cap=len。

除了使用make来初始化slice外,还可以通过数组slice化的方式,即通过语法u[low:high]的方式,这种方式也被称为slice的reslicing。

u := [10]int{11,12,13,14,15,16,17,18,19,20}
s := u[3,7]

表示如下:

slice共用数组

你发现什么了吗?

如果没有,再看一张图

多个slice可以共同一个底层数组

高级特质

如果slice仅仅是这点能力,显然与他的身份不匹配。

slice有一个重要的特点:动态扩容。即可以通过内置函数append对slice进行赋值操作。

var s []byte 
s = append(s,1)

slice是如何进行动态扩容的呢?别急,我们来看一段代码。

var s []int

s = append(s,11)
fmt.Println(len(s),cap(s)) // 1 1

s = append(s,12)
fmt.Println(len(s),cap(s)) // 2 2

s = append(s,13)
fmt.Println(len(s),cap(s)) // 3 4

s = append(s,14)
fmt.Println(len(s),cap(s)) // 4 4

s = append(s,15)
fmt.Println(len(s),cap(s)) // 5 8

从上面代码可以看出,len的值符合我们预期,是线性增长的,但cap的值就不怎么规律了。直接看代码可能不直观,通过下图可以更清晰的看出多次append操作,slice是如何扩容的。

slice扩容过程

现在可以得出结论:在当前底层数组容量无法满足的时候,会动态分配新的数组,新数组的长度会按照一定的算法扩增,并在新数组建立好之后,将原来数组的元素拷贝到新数组上来,原来数组在后续的GC中回收掉。

扩容算法如下:

func growslice(et *_type, old slice, cap int) slice {
	...

	if et.size == 0 {
		return slice{unsafe.Pointer(&zerobase), old.len, cap}
	}

	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold {
			newcap = doublecap
		} else {
			for 0 < newcap && newcap < cap {
				newcap += (newcap + 3*threshold) / 4
			}
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
	...
}

不得不提的copy函数

copy函数是Go语言提供的内置函数,用来将源slice的元素复制到目标slice中。

格式如下:

copy( destSlice, srcSlice []T) int

copy函数有两个参数:第一个是目标slice,第二个是源slice。也就是将 srcSlice 复制到 destSlice。返回值为实际发生复制元素的个数。

下面代码展示了copy函数的使用过程

s1 := []int{1, 2, 3, 4, 5}
s2 := []int{5, 4, 3}
copy(s2, s1) // 只会复制s1的前3个元素到s2中 s2=[1 2 3]
copy(s1, s2) // 只会复制s2的3个元素到s1的前3个位置 s1=[1 2 3 4 5]

两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题

附上开头的答案,你对了吗?有没有反直觉的地方?

len1: 0  0
len2: 5  10
len3: 8  10
len4: 5  10
content1: [6 6 6 6 0 1 2 3] [6 6 6 6 0]
len5: 11  24
content2: 6 5

Tags:

最近发表
标签列表