Slice详解
slice和数组的区别
众所周知,go是一门强类型的语言,什么是强类型呢?就是对类型要求非常严格(在运算时),所以go中的float不能和int进行运算,甚至int和int64也不能进行运算。
数组是指长度固定的数据集合,比如 [3] int 指的就是长度为3的int类型集合,它和[4] int是两个完全不同的类型,所以不能作比较,比较也是一种运算。
而slice则是动态数组,长度不固定,可以动态扩容,slice的类型和长度没有关系,所以不同的slice可以进行比较(但这个操作通常没有意义)
slice本质
slice其实就是一个结构体,里面有着对数组的封装,还有len和cap两个字段来描述数组的长度和容量
1 | // runtime/slice.go |
如果要判断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 | // go 1.9.5 src/runtime/slice.go:82 |
go 1.18源码
1 | // go 1.18 src/runtime/slice.go:178 |
如果只看前半部分,那么网上的各种文章说的是对的,现实是,后半部分还对newcap做了一个内存对齐,这个和内存分配策略有关,进行内存对齐之后,新slice的容量>=未进行内存对齐之前的cap
new和make的区别
首先new和make都是Go内置的用来分配内存的函数,区别是make用来给slice map channel等引用类型分配内存,返回值是一个值类型,而new用来给数组、结构体值类型来分配内存,后者返回值是一个指针