slicego中比较简单的一个数据结构了(当然,string更加简单)。

先看其定义:

1
2
3
4
5
type 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
2
s1 := make([]User, 10, 20)
s2 :=append(s1[::len(s1)], User{}) // s1[::len(s1)]会创建一个cap等于len的切片,这样append就会发生扩容

在平常的开发中,我们经常使用到切片的一个场景是数据库分页查询,创建一个slice,然后把返回的结果集装到这个slice中。

那么在这个场景下我们应该怎么用好slice呢?

首先,因为我们是分页查询,结果集的大小是已知的,因此我们可以预分配切片大小

然后,是使用[]User好还是[]*User好呢?

因为go的内存管理会将mspan按照sizeClass进行划分,从8byte32kb,并且会在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
5
var 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
2
var 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
6
func mallocgc(size uintptr, typ *_typ, needzero bool) unsafe.Pointer {
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
}