0%

Slice详解

Slice详解

slice和数组的区别

众所周知,go是一门强类型的语言,什么是强类型呢?就是对类型要求非常严格(在运算时),所以go中的float不能和int进行运算,甚至int和int64也不能进行运算。

数组是指长度固定的数据集合,比如 [3] int 指的就是长度为3的int类型集合,它和[4] int是两个完全不同的类型,所以不能作比较,比较也是一种运算。

而slice则是动态数组,长度不固定,可以动态扩容,slice的类型和长度没有关系,所以不同的slice可以进行比较(但这个操作通常没有意义)

slice本质

slice其实就是一个结构体,里面有着对数组的封装,还有len和cap两个字段来描述数组的长度和容量

1
2
3
4
5
6
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针 指向的其实是一个底层的数组
len int // 长度
cap int // 容量
}

如果要判断slice是否为空,要使用

len(slice)==0

而不能使用

slice==nil

因为slice=0判断的是数组内是否有元素,如果没有元素则为空,而slice==nil判断的是一整个结构体是否为Nil 如果我们用var 的方式声明,不会给slice分配内存,那么slice确实=nil。但如果我们使用make的方式进行声明,那么就会给slice分配内存,所以slice就!=nil了

同理的 要判断两个slice是否相同,不能简单的通过slice1==slice2进行判断,而是要循环切片进行判断

slice传参

在我们把slice作为参数传递出去的时候,传的是,这也就是为什么我们在被调函数中对数组进行append,在主调函数中看不到这个变化。但是如果直接通过下标的方式对slice进行修改,那么是可以反映到主调函数中的(因为下标修改是直接对底层的数组进行修改)

准确的来说,go中所有的参数传递,都是值传递,并没有引用传递,那有的同学可能就疑惑了,我传递map的话不是在被调函数中的改变可以反映到主调函数中吗?

因为进行函数调用的时候,slice类型会调用runtime.makeslice函数,这个函数的返回值类型是值,而map类型会调用runtime.makemap函数,这个函数的返回值类型是一个指针

slice的扩容过程

网上流传的版本是:当slice容量小于1024的时候,每次扩容翻倍,在1024长度之后,每次扩容1.25倍,而在1.18版本之后变为了,当容量小于256的时候,扩容为两倍,超过256,newcap=oldcap+(oldcap+3*256) /4

这个说法不对,或者说只对了一半

go 1.9.5源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// go 1.9.5 src/runtime/slice.go:82
func growslice(et *_type, old slice, cap int) slice {
// ……
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for newcap < cap {
newcap += newcap / 4
}
}
}
//
//内存对齐
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
}

go 1.18源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// go 1.18 src/runtime/slice.go:178
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 {
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
}
if newcap <= 0 {
newcap = cap
}
}
}

//内存对齐
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
}

如果只看前半部分,那么网上的各种文章说的是对的,现实是,后半部分还对newcap做了一个内存对齐,这个和内存分配策略有关,进行内存对齐之后,新slice的容量>=未进行内存对齐之前的cap

new和make的区别

首先new和make都是Go内置的用来分配内存的函数,区别是make用来给slice map channel等引用类型分配内存,返回值是一个值类型,而new用来给数组、结构体值类型来分配内存,后者返回值是一个指针