网站首页 > 知识剖析 正文
每当你花费大量时间使用某种特定工具时,深入了解它并了解如何高效地使用它是很值得的。
最近,看到一个关于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语言中,数组是一个固定长度的、存储同构类型元素的连续序列。根据描述,可以得知,数组有两个属性:类型、长度。这两个属性相同的数组是等价的。
比如下面的a、b、c是三个不同的数组。其中,a、b长度相同,但类型不同,a、c类型相同,但长度不同。
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
猜你喜欢
- 2024-11-10 PHP数组学习笔记(1) php数组有哪几种类型
- 2024-11-10 Rust语言入门教程 数组和切片 rust语言例子
- 2024-11-10 javascript自学笔记:Array类型1 javascript自学笔记:array类型1怎么解决
- 2024-11-10 Array.from详解: 语法、功能与应用场景
- 2024-11-10 帮你精通JS:解析与盘点数组array的5类22种方法
- 2024-11-10 10 个实用的 JS 技巧 js常用方法大全
- 2024-11-10 WordPress 内置的数组处理相关函数大全
- 2024-11-10 3分钟短文 | PHP获取函数的代码片段,唯有反射最高效
- 2024-11-10 JS 中的类数组对象如何转换为数组?
- 2024-11-10 Go 中的循环是如何转为汇编的?看完你懂了吗?
- 最近发表
- 标签列表
-
- xml (46)
- css animation (57)
- array_slice (60)
- htmlspecialchars (54)
- position: absolute (54)
- datediff函数 (47)
- array_pop (49)
- jsmap (52)
- toggleclass (43)
- console.time (63)
- .sql (41)
- ahref (40)
- js json.parse (59)
- html复选框 (60)
- css 透明 (44)
- css 颜色 (47)
- php replace (41)
- css nth-child (48)
- min-height (40)
- xml schema (44)
- css 最后一个元素 (46)
- location.origin (44)
- table border (49)
- html tr (40)
- video controls (49)