声明
本系列文章并不会停留在Go语言的语法层面,更关注语言特性、学习和使用中出现的问题以及引起的一些思考。
要点
本文关注Go语言数组和切片相关的语言特性。
数组和切片以及字符串的关系
相同点
Go语言中数组、字符串和切片三者是密切相关的数据结构。这三种数据类型,在底层原始数据有着相同的内存结构,在上层,因为语法的限制而有着不同的行为表现。
差别
- Go语言的数组是一种值类型,虽然数组的元素可以被修改,但是数组本身的赋值和函数传参都是以整体复制的方式处理的。
- Go语言字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。字符串赋值只是复制了数据地址和对应的长度,而不会导致底层数据的复制。
- Go语言切片的行为更为灵活,切片的结构和字符串结构类似,但是解除了只读限制。切片的底层数据虽然也是对应数据类型的数组,但是每个切片还有独立的长度和容量信息,切片赋值和函数传参数时也是将切片头信息部分按传值方式处理。因为切片头含有底层数据的指针,所以它的赋值也不会导致底层数据的复制。
给大家贴个数组切片关系图:
空切片和nil切片不是一个切片
var (
a []int // nil切片, 和 nil 相等, 一般用来表示一个不存在的切片
b = []int{
} // 空切片, 和 nil 不相等, 一般用来表示一个空的集合
)
其中:空切片和 nil 切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。
空切片会分配底层数组么?
在Go语言中我们通常会以[]int{}的方式去定义一个切片,这种切片我们称为空切片,我们知道,它是一个空的集合,那么他会分配相应的底层数组的内存空间么?我们来看一下下面这个例子:
func main() {
var c = make( []int , 0 )
pc := (*reflect.SliceHeader)(unsafe.Pointer(&c))
fmt.Println(pc.Data) //824634224416
}
我们会发现,它是会指向一个地址的,那么它指向的地址会是底层数组的地址么?我们看一下源码是怎么样的?
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
}
}
根据上边源码发现,在切片大小为0时,直接返回了一个内存地址,这点我们需要注意上边的那个结论:空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。
append对于数组和切片
首先,数组是不能使用append方法的,也就是说数组在声明的时候声明了长度之后是没办法扩容的,而slice可以,这也是为什么大家都普遍使用slice的原因。我们看下代码:
func main() {
var a = []int{
1,3,4,5}
a = append(a, 5)
fmt.Println(a) //[1 3 4 5 5]
var b = [4]int{
1,3,4,5}
b = append(b, 5) //cannot use 'b' (type [4]int) as type []Type
}
既然扩容是存在的,那么他也有一定的规则:在添加元素时候,若原本slice的容量不够了,则会自动扩大一倍cap,在扩大cap时候是将原来元素复制一份(而不是引用),即这种情况下原有的不会变。
func main() {
var a = []int{
}
//数组和切片len,cap,数组len和cap是定义的长度,切片是len是实际值,cap是数组容量
fmt.Println(cap(a)) //0
a = append(a, 5)
fmt.Println