概述
在使用 Go 开发的时候,数组和切片经常被使用到,这篇文章来简单聊聊吧。
数组 array
在 Go 中,有两种方式可以初始化数组
func main() { userId := [3]int{1, 2, 3} userName := [...]string{"wqq", "curry", "joke"} }
一种是显式的定义数组的大小,另一种通过 […] 声明数组,Go 会在编译期间推导出数组的大小。
既然使用了数组,少不了遍历,在 Go 中遍历数组一般也就两种方式。
func main() { userIds := [3]int{1, 2, 3} names := [...]string{"wqq", "curry", "joke"} for i := 0; i < len(names); i++ { fmt.Printf("user id is:%v,user name is:%v\n", userIds[i], names[i]) } for index, item := range names { fmt.Printf("user id is:%v,user name is:%v\n", userIds[index], item) } }
第一种就是你所认知的 for 循环。第二种可以使用 for/range 表达式,该表达式返回两个值,第一个值是索引,第二个值对应此索引的元素值。range 不单单能遍历数组,还能遍历 slice、map、channel 等集合结构。当然这些不在这篇文章的讨论范围内。
切片 slice
切片本质上是动态数组,它的底层包含了对数组的引用。切片的长度是动态的,可以随意的对其进行 append 操作,在使用的过程中,如果容量不足,会自动进行扩容操作。我们可以从源码看看 slice 的结构。源码位于 src/runtime/slice.go ,更多底层知识可以自行查看源码。
type slice struct { array unsafe.Pointer // 底层数组的指针位置 len int // 切片当前长度 cap int //容量,当容量不够时,会触发动态扩容的机制 }
同理,初始化 slice 的方式也是多样的。
使用 make 关键字
和数组一样,使用字面量初始化
通过下标的方式获取数组或者切片的一部分,生成 slice
func main() { // 字面量初始化 userIds1 := []int{1, 2, 3} // make初始化slice的长度为5,容量为10 userIds2 := make([]int, 5,10) // 通过下标的方式获取数组的一部分作为alice userArray := [5]string{"curry", "wqq", "lisa", "tony", "james"} // 获取从索引下标0开始,到下标3(不包括3) user := userArray[0:3] fmt.Printf("userIds1:%v,userIds2:%v,userSlice:%v\n", userIds1, userIds2, user) }
这里就拿 make 初始化切片进行说明。
注:图片来源 《Go编程专家》
这段初始化操作表示 slice 的长度是 5,容量是 10,array 字段存储的是引用数组的指针位置。因为长度是 5,我们可以使用下标 0-4 来操作此 slice。同时容量是 10,所以后续向 slice 添加新数据暂时不需要重新分配新内存。
那数组和切片有什么关联呢?
我们看看通过下标的方式获取数组数据,初始化切片的一种形式。
func main() { userArray := [4]string{"curry", "wqq", "lisa", "tony"} // 获取从索引下标0开始,到下标3(不包括3) userSlice := userArray[0:3] userSlice[0] = "zhangsan" fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice) }
我们用数组创建了 userSlice 的切片,此时 userSlice 将和 userArray 共用一部分内存。因此在修改 userSlice 索引 0 处的值时,操作的是同一块数组内存地址,从结果中可以看出生效了。
然后我们开始往 userSlice 切片添加元素。
func main() { userArray := [4]string{"curry", "wqq", "lisa", "tony"} // 获取从索引下标0开始,到下标3(不包括3) userSlice := userArray[0:3] userSlice[0] = "zhangsan" fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice) userSlice = append(userSlice, "test1") fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice) }
查看输出结果:
可以看到,再向 userSlice 增加一个元素后,打印结果,数组和切片值一样,操作之后 userSlice 的 len 是 4,数组的长度也是 4。操作 append 后 userSlice 底层数组和 userArray 指向的还是同一个内存地址,并不需要发生扩容。
这时候,userSlice 所引用的底层数组已经满了 (底层数组的长度是 4),我们继续向 userSlice 增加元素。
func main() { userArray := [4]string{"curry", "wqq", "lisa", "tony"} // 获取从索引下标0开始,到下标3(不包括3) userSlice := userArray[0:3] userSlice[0] = "zhangsan" fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice) userSlice = append(userSlice, "test1") fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice) userSlice = append(userSlice, "test2") fmt.Printf("userArray:%v,user:%v\n", userArray, userSlice) }
查看输出结果:
可以看到,userArray 的元素未变,因为这时候 userSlice 切片的长度已经大于原指向的数组的长度了, userSlice 发生了扩容。
我们可以做个实验测试一下,我们修改数组 userArray 范围内的 userSlice 元素的值,查看数组的数据是否会跟着改变。
func main() { userArray := [4]string{"curry", "wqq", "lisa", "tony"} // 获取从索引下标0开始,到下标3(不包括3) userSlice := userArray[0:3] userSlice[0] = "zhangsan" fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice) userSlice = append(userSlice, "test1") fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice) userSlice = append(userSlice, "test2") fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice) // 改变索引0处的值 userSlice[0] = "only one" fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice) }
最后一行已经说明了一切。此时的 userSlice 发生了扩容,不再和 userArray 共用原数组空间了。因此对 userSlice 的改动不会影响到 userArray。
关于扩容
前面提到在向切片添加新元素时如果此时切片的容量不足,会自动发生扩容。所谓扩容,也就是为当前切片生成新的一块内存空间,然后根据一定规则,将原切片的元素全部拷贝到新的地址。扩容的规则在 src/runtime/slice.go 里的 growslice 方法。
这里截取了此方法中关于扩容规则的代码。
如果期望的新容量 (cap) 大于当前容量的两倍,那么就直接使用期望的容量
如果当前切片的长度 (len) 小于 1024,那么把当前容量翻倍
如果当前切片的长度 (len) 大于等于 1024,那么每次把当前容量增加 1/4,直到新容量值大于期望的的容量。
其实要写下去还有很多东西,比如,sliceCopy、底层编译逻辑……,有些东西我也没看过,学习的最好方式还是自己动手然后输出。