slice
是go
中比较简单的一个数据结构了(当然,string
更加简单)。
先看其定义:
1 | type slice struct { |
可以看到切片实际上有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 | s1 := make([]User, 10, 20) |
在平常的开发中,我们经常使用到切片的一个场景是数据库分页查询,创建一个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 | var s []int // s == nil |
像上面的操作,虽然s
等于nil
,但是这些操作都可以正常执行,因此我们在平时的使用中,并不需要刻意关心切片是否等于nil
。
但是当json
序列化时,如果切片为nil
时,对应序列化为null
,而如果是非nil
的空切片,则对应为[]
。
当我们声明一个变量时:
1 | var s1 []int |
因为初始化为零值,则对应的切片结构体三个字段都是零值,因此s1==nil
而当我们这样写时:
1 | var s1 = []int{} |
这两个切片都是非nil
的空切片。而又因为切片的cap
是零,go
会将其data
字段初始化为:
1 | // base address for all 0-byte allocations |
zerobase
是声明在runtime
包下面的一个全局变量,当需要分配一块零字节的内存时,都会返回该变量的地址:
1 | func mallocgc(size uintptr, typ *_typ, needzero bool) unsafe.Pointer { |