slice
是go
中比较简单的一个数据结构了(当然,string
更加简单)。
先看其定义:1
2
3
4
5type slice struct {
array unsafe.Pointer // 对应底层数组
len int // 切片当前长度,也就是具有的元素个数
cap int // 底层数组的实际长度
}
可以看到切片实际上有3个字段,第一个是其底层数组,第二个是当前具有的元素个数,我们可以直接使用下标[0,len)
来访问他们,而cap
字段是底层数组的长度。
我们可以使用make
来创建一个切片:1
s1 := make([]User,0,10)
表示创建一个长度是0,容量是10的User切片,然后我们可以使用内置函数append
来追加元素:1
s1 = append(s1, User{})
使用append
,如果切片的底层数组如果还有足够的容量,则可以直接扩展len
字段,否则会发生切片的扩容,具体可以参考切片扩容
因为slice
本质上是一个结构体,是一个值类型,而append
需要更改内部状态,因此会创建一个新的切片返回。
如果发生了扩容,那么新返回的切片的底层数组和原来的底层数组是两个不同的数组,否则会共享同一个底层数组。
如果我们需要append可以返回一个有独立底层数组的切片,则可以:1
2s1 := make([]User, 10, 20)
s2 :=append(s1[::len(s1)], User{}) // s1[::len(s1)]会创建一个cap等于len的切片,这样append就会发生扩容
在平常的开发中,我们经常使用到切片的一个场景是数据库分页查询,创建一个slice
,然后把返回的结果集装到这个slice
中。
那么在这个场景下我们应该怎么用好slice
呢?
首先,因为我们是分页查询,结果集的大小是已知的,因此我们可以预分配切片大小。
然后,是使用[]User
好还是[]*User
好呢?
因为go
的内存管理会将mspan
按照sizeClass
进行划分,从8byte
到32kb
,并且会在P
中缓存每个级别的mspan
(每个P中都有一个mcache,mcache中针对每种sizeClass缓存两个mspan,一个只用于分配不需要gc扫描的对象,这样整个mspan中的page都不需要扫描)。
因此,如果对象的大小不超过32kb
(实际上我们很少会用到那么大的对象),那么内存分配的效率与分配的大小并没有多大关系。而且本身频繁的内存分配也是一种性能损耗。
因此,推荐的是使用[]User
类型。这样只会分配一次内存。而如果使用[]*User
类型,需要分配n+1次,底层数组1次加上n次切片元素。
然而,事与愿违的是,我们一般会使用第三方orm
框架,这些框架中,会循环的通过反射去new
对象,然后append
进去。
那么切片什么时候等于nil
呢?
slice
本身本质上是一个结构体,只有当其三个字段都是零值的时候,才等于nil
。
在代码中,我们判断一个slice
是否为空,并不需要关心其是否是nil
,只要使用len(s) == 0
就可以了1
2
3
4
5var s []int // s == nil
s1 = append(s, 1) // 发生扩容
s2 = s[:0] // s2 != nil
像上面的操作,虽然s
等于nil
,但是这些操作都可以正常执行,因此我们在平时的使用中,并不需要刻意关心切片是否等于nil
。
但是当json
序列化时,如果切片为nil
时,对应序列化为null
,而如果是非nil
的空切片,则对应为[]
。
当我们声明一个变量时:1
var s1 []int
因为初始化为零值,则对应的切片结构体三个字段都是零值,因此s1==nil
而当我们这样写时:1
2var s1 = []int{}
var s2 = make([]int, 0, 0)
这两个切片都是非nil
的空切片。而又因为切片的cap
是零,go
会将其data
字段初始化为:1
2// base address for all 0-byte allocations
var zerobase uintptr
zerobase
是声明在runtime
包下面的一个全局变量,当需要分配一块零字节的内存时,都会返回该变量的地址:1
2
3
4
5
6func mallocgc(size uintptr, typ *_typ, needzero bool) unsafe.Pointer {
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
}