和C、C++中的Array数组一样,在Golang中数组也是定长的,每次定义的时候大小就已经固定了,而这也意味着数组的具有一定的局限性。为提高数组的灵活性,C++中可以用vector,Java中可以选择ArrayList,而在Golang中与他们对应的便是Slice啦。
基础用法
初始化
- 使用make初始化,
make([]type,len,cap)
其中cap参数可以省略,省略后其默认值等于len。
slice := make([]int,3,6) // 使用make初始化Slice
slice := make([]int,3) // 省略cap
- 可以通过具体的元素来初始化,其默认的len和cap就是元素的个数。
slice := []int{3,4,5}
- 使用已有的切片或者数组进行初始化,
oldSlice[start:end]
,这种方法可以理解为将已有的切片或者数组进行截取(左闭右开),start和end省略时分别表示从第一个元素开始、到最后一个元素结束。
slice := old[3:6]
slice := old[3:]
slice := old[:6]
slice := old[:]
增加元素
- 使用append函数添加元素。要添加元素的个数可以是1个或以上。
slice = append(slice,6,7,8)
- append也可以将两个slice合并,这里的
old
也是一个slice,在它后面加三个点就可以将其内部元素取出来作为append的参数。
slice = append(slice,old...)
删除元素
- Slice删除元素的操作可以使用截取的方式。
slice = slice[3:6]
底层结构
咱先来看一下Slice有哪几部分组成:
type slice struct {
array unsafe.Pointer
len int
cap int
}
- array: 它的类型是unsafe.Pointer用于指向存储实际数据的数组的指针。这块有点绕,咱们可以简单理解为它就是一个数组,而数组的特点就是在内存中是连续存储的。
- len: 它是指当前切片中元素的数量。值得注意的是从字面意思上理解它是长度,但是为了和 ‘数组’的‘长度’做区分,我更喜欢称它为“元素的数量”。
- cap: 它是指当前切片的容量,也就是array数组已分配内存的长度。
以上三个参数就构成了我们常用的Slice,看似简单的Slice其实也有很多细节,我们将从Slice的使用角度去分析其底层逻辑。
Slice的初始化
Slice的初始化可以有一下几个方式:
- 使用make初始化。
- 通过已存在的切片或者数组初始化。
- 通过具体元素初始化。
slice := make([]int,3)
执行以上代码后,可以在内存中得到这样一个Slice:
当我们传入的len为3的时候,那么Slice的前三个元素的value都会被初始化为对应 type
的0值,如果是复杂类型的数据初始化的值则是 nil
。另外需要注意的是我们在make中并没有传入cap的容量,所以这时的cap容量是等于len的,如果我们对其进行append操作就会触发Slice的扩容了。当然我们在make的时候可以传入初始的cap容量:make([]int,3,6)
那么此时的cap为6,再进行append就不需要额外扩容了。
Slice的遍历
上文我们介绍到Slice在初始化的时候cap是可以省略的,但是len可不行。而len这个参数也是Slice最为重要的参数之一,从何说起呢?
通过上文的例子可以看到当我们将len设置为3,cap容量设置为6的时候,在内存中其实已经分配好了,但是如果我们想访问下标为3
的元素,那就会抛出”下标越界”,这是因为Slice访问元素的边界是由len决定的。尽管内存中已经为3,4,5
分配好了内存空间,但此时的len为3,那么我们只能访问0,1,2
这三个下标的元素,同样当我们使用for循环对其进行遍历的时候也是以len作为边界。
Slice的扩容规则
由于Slice是在已有的数组空间上进行存储数据,因此就意味着一定会有数据已经存满了数组空间的情况,其实也是对应了cap == len
的情况,那么此时再进行append
就需要对Slice进行扩容,底层实现就是重新创建一个足够长的数组,再将原来的数据拷贝过去。
slice = append(slice,10)
我们在len=3,cap=3的一个Slice上进行添加元素操作就会发生一次扩容。
当我们进行一次append的时候,可以看到len加了1变成了4,cap容量从3变成了6。这里咱就可以讨论一下Slice的扩容机制啦!在进行append操作时,如果当前容量不足以存储新的元素那么就会发生扩容,扩容会调用一个叫做growslice
的函数,它返回的是一个新的Slice,所以我们只要分析一下growslice
的代码就可以知道Slice的扩容规则了。由于1.18版本之后的扩容机制发生了些许改变,因此我们需要分开讨论。
- 1.18之前 我们可以具体看一下
growslice()
函数中关于cap的代码:
// src/runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
// ......
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.cap < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
// ......
return slice{p, old.len, newcap}
}
这里growslice
函数中的cap参数指的就是扩容的期望容量,而通过分析代码不难看出:
- 如果期望容量大于当前容量的2倍,则新Slice的容量就是期望容量的大小。
- 如果当前容量小于1024,那么新Slice的容量则是原来的2倍。
- 如果当前容量大于1024,那么新Slice的容量每次增加25%,直到新容量
>=
期望容量。
- 1.18之后
// src/runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
// ......
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
// ......
return slice{p, old.len, newcap}
}
在1.18过后原本1024的阈值修改成了256,扩容规则也略微发生了变化:
- 如果期望容量大于当前容量的2倍,则新Slice的容量就是期望容量的大小。
- 如果当前容量小于256,那么新Slice的容量则是原来的2倍。
- 如果当前容量大于256,那么新Slice的容量每次增加
旧容量+3*256
的 25%,直到新容量>=
期望容量。
使用细节
了解了底层结构,我们可以使用一个例子来加深一下对Slice印象,这样也可以帮助我们更好地理解Slice。
Slice在函数中传递是”值传递“还是”引用传递“?
func change(course []string) {
course[0] = "java"
}
func main() {
course := []string{"go", "grpc", "orm"}
change(course)
fmt.Println(course)
}
//输出:[java grpc orm]
实践出真知,我们简单写了几行代码,把course传递到change函数中,修改在change中的第0个元素,最后在主函数中将course输出。
可以发现输出的结果是[java grpc orm]
,course被修改了,所以我们可以初步得出结论:Slice在Golang中是“引用传递”。但真的是这样嘛?
func change(course []string) {
course = append(course, "java")
}
func main() {
course := []string{"go", "grpc", "orm"}
change(course)
fmt.Println(course)
}
//输出:[go grpc orm]
我们简单修改一下代码,在change函数中使用append给course添加一个元素。这时输出的却是[go grpc orm]
,可我们明明添加了一个元素啊?去哪了呢?
func change(course []string) {
course = append(course, "java")
fmt.Println("change :",course)
}
在change和main中分别打印输出一下,change中输出了change :[go grpc orm java]
。那么问题来了,为什么change中的course可以正常输出java
,而main中的course就没有呢?这不是值传递的特性嘛,可我们刚刚得出的结论明明是引用传递呀!
实际上Slice的函数传递是值传递,当我们将course传递给change函数时,拷贝了一份course中的内容到change函数中,此时就有了两个Slice,而Slice中的array是一个指针,因此两个Slice都指向了同一个数组空间。
还记得我们讲的扩容机制嘛?当Slice进行扩容的时候,需要重新开辟一块内存空间,将旧值复制进去,因此在change中的course它会开辟一块新的内存用于存放数据,并且指向它,但是main中的course指向的数组地址还是原来的,因此就读不到java
这个数据了。