Golang中的「数组」和「切片」都是存储同一数据类型的容器,只不过Golang中的数组长度是固定的,而切片的长度是可变化的。我们日常应用中还是切片的占比还是比较大的,本篇文章我们来详细探讨下这两种数据类型
Golang中的数组长度是固定的,并且在声明时就必须指定他的长度
func main() {
// 声明数组
var a1 [2]string
// 声明时并初始化数组
// 这里我声明了,string类型的数组a,他的长度为2
a2 := [2]string{"a", "b"}
// 输出 a1 和 a2 的 len(长度)和cap(容量)
fmt.Printf("a1的长度:%d ,容量:%d \n", len(a1), cap(a1))
fmt.Printf("a2的长度:%d ,容量:%d \n", len(a2), cap(a2))
}
输出
a1的长度:2 ,容量:2
a2的长度:2 ,容量:2
可以看出,数组的长度和容量与你创建数组时指定的长度保持一致,
还有一个问题就是Golang中数组类型是「值类型」,下面我们通过代码来详细描述下
func main() {
a1 := [2]string{"a", "b"}
fmt.Printf("a1 的指针:%p,a1的值:%v \n", &a1, a1)
arrayTest1(a1)
a2 := a1
fmt.Printf("a2 的指针:%p,a2的值:%v \n", &a2, a2)
}
func arrayTest1(arr [2]string) {
arr[0] = "aaaa"
fmt.Printf("arrayTest1中arr的指针:%p,arr的值:%v \n", &arr, arr)
}
// 输出
// a1 的指针:0x14000070000,a1的值:[a b]
// arrayTest1中arr的指针:0x14000070040,arr的值:[aaaa b]
// a2 的指针:0x14000070080,a2的值:[a b]
可以看出赋值语句「a2 := a1」以及函数调用「arrayTest1(a1)」中的指针都不一样,并且在函数中修改数组的值也不影响a2。也就是说这两行代码仅仅只是一次值copy,可以结合下图一起理解
Golang中切片底层数据结构其实包含的还是数组,说直白点就是切片其实是对数组的连续片段的引用,所以我们经常说切片是「引用类型」
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 当前切片的长度
cap int // 当前切片的容量,cap >= len
}
func main() {
a1 := make([]int, 2)
fmt.Printf("a1的长度为:%d,容量为:%d \n", len(a1), cap(a1))
}
// 输出
// a1的长度为:2,容量为:2
在使用make声明并初始化切片a1时,make第二个参数我设置成了2,其实就是指定他的长度为2。通过打印我们得知,当我们不指定容量(cap)属性时,切片的容量和长度保持一致。反之如果指定容量的话,则以指定的为准
func main() {
a2 := make([]int, 2, 3)
fmt.Printf("a2的长度为:%d,容量为:%d \n", len(a2), cap(a2))
}
// 输出
// a2的长度为:2,容量为:3
上文中我们知道切片的底层其实是一个数组,当我们使用make初始化数组并指定容量时,cap就代表了底层数组的长度,而len属性则表明了我要对哪些片段的引用
func main() {
a1 := []int{1, 2, 3, 4}
fmt.Printf("a1的长度为:%d,容量为:%d \n", len(a1), cap(a1))
}
当我使用字面量的形式初始化切片时,那么切片的底层数组就是当前字面量,同时他的len和cap属性与字面量长度保持一致
func main() {
a1 := []int{1, 2, 3, 4}
fmt.Printf("a1的长度为:%d,容量为:%d,值为:%v \n", len(a1), cap(a1), a1)
a2 := a1[1:3]
fmt.Printf("a2的长度为:%d,容量为:%d,值为:%v \n", len(a2), cap(a2), a2)
}
// 输出
// a1的长度为:4,容量为:4,值为:[1 2 3 4]
// a2的长度为:2,容量为:3,值为:[2 3]
「a2 := a1[1:3]」这行代码我通过切片表达式来给切片a2进行了一个赋值操作,而[1:3]是一个前闭后开区间[1:3),所以我们很容易能够知道a2的长度为2,他的值为[2,3]
按照我们之前的说法是切片的容量等于其底层数组的长度,那么这里a2的容量应该是4才对,为什么输出的是3呢?
我们上面说的「切片的容量等于其底层数组的长度」仅仅只适用于通过「make」和「字面量」形式初始化切片的场景,而a2是通过切片表达式来进行初始化的,而a2的cap属性其实是通过切片表达式最多能够看到底层数组的元素个数,这里的窗口(索引)从1开始,而原底层数组的长度为4,由4-1得知,a2的cap为3。a2的len属性其实是当前窗口内[1:3],a2能够看到底层数组的哪些元素
我们前面稍微提了下,切片在Golang中属于引用类型,这里我们用代码进行验证下
func main() {
a1 := []int{1, 2, 3, 4}
fmt.Printf("a1的指针:%p,值为:%v \n", a1, a1)
testSlice(a1)
a2 := a1
fmt.Printf("a2的指针:%p,值为:%v \n", a2, a2)
}
func testSlice(slice []int) {
slice[0] = 111
fmt.Printf("slice的指针:%p,值为:%v \n", slice, slice)
}
// 输出
// a1的指针:0x1400001e0a0,值为:[1 2 3 4]
// slice的指针:0x1400001e0a0,值为:[111 2 3 4]
// a2的指针:0x1400001e0a0,值为:[111 2 3 4]
可以看出三者的指针都一致,也即指向了同一个底层数组。为什么可以肯定呢,因为我再testSlice函数中修改了slice中的第一个元素值为111,a2中的第一个元素也变成了111
上述文章我们了解了Golang中的「数组和切片」,其实最重要的是我们应该知道在什么样的场景下来使用这两种数据类型
当你明确知道数据长度时,你可以使用数组。因为数组的定长特性,从而数组可以开辟一块连续的内存空间来提升按索引的访问效率,而且也可以避免频繁的扩容导致的内存碎片
当不确定元素长度时,并且随时可能进行追加元素的操作。
这里有个小小的建议是,当你创建切片时可以大致预估下元素的长度,从而给切片指定cap属性,尽量减少因元素的增加导致的底层数组扩容问题