go实战读书笔记(五):Slice 切片
Contents
上一篇讲了数组, 但数组有个局限, 就是一旦数组被声明, 大小不可变. 切片弥补了数组的短板,可以按需求自动增长缩小, 可以当做动态数组使用。
*切片的动态增长通过内置函数 append 来实现, 动态缩小通过对切片进行进一步切分.*
切片的内部构成
切片是一个很小的对象, 切片由三个字段构成:
- 指向底层数组的指针.
- 切片包含的元素个数. (长度)
- 切片允许的容量.
1.1 切片构造示意图:
切片的创建和初始化
通过
make
创建// 创建一个字符串切片, 默认长度为3, 容量为5. slice := make([]string, 3, 5)
在make函数里面, 第一个参数是切片类型, 第二个是底层数组的初始长度, 第三个是底层数组的容量.
通过切片字面声明创建
// 创建字符串切片, 其长度和容量都是5. slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
创建空切片:
切片的0值是
nil
, 所以如果想要创建一个nil切片, 创建的时候, 不需要赋值即可.var slice []string // or slice := make([]string, 0) // or slice := []string{}
2.1 空切片结构示意图
- 函数要求返回一个切片, 但错误发生, 可返回空切片.
- 数据库查询返回0个结果.
使用切片
赋值: 切片里面赋值跟数组里面的操作一致. e.g.
// 创建一个长度为5的整型切片. slice := []int{1,2,3,4,5} // 把第二个元素的值改为6. slice[1] = 6
切片这个名字有个好处, 是将给定切片或者数组切分就可以创建一个新的切片 e.g.
// 创建一个新的长度为5的整型切片. slice := []int{10,20,30,40,50} // 基于给定切片, 创建一个新的长度为2, 容量为4的切片. newSlice := slice[1,3]
3.1 上述两个切片内部结构示意图
Tips: 计算切片长度和容量.
对于底层数组容量是k的切片slice[i:j]来说:
- 长度: j-i
- 容量: k-i
这里要引起注意的是, 前面讲过, 切片内部三个数据最主要的就是指向数组的指针. 因为指针的缘故, 所以在这里, 当修改任一一个切片中某一个成员时, 其它跟其共享底层数组的切片内部成员也会发生变化.
切片增长: 切片比起数组最大的优势就是可以动态增长. 切片通过go内置方程
append
来实现切片的增长. 注意:- 当append时, 增加新的元素后, 没有超过原切片的容量, 那新元素会被安置到对应底层数组里.
- 当append时, 增加新的元素后, 超过了原切片的容量, 那么go会新建一个容量更大新的切片, 并把原切片的数据拷贝到新切片里, 在添加要增加的新元素.
第一种情况:
// 创建一个新的长度为5的整型切片. slice := []int{10,20,30,40,50} // 基于上述切片, 创建一个新的长度为2, 容量为4的切片. newSlice := slice[1,3] newSlice = append(newSlice. 60)
根据之前的容量公式,
newSlice
的长度是3-1=2, 容量是 5-1 = 4. 所以在append的时候, newSlice 还有足够容量, 因此在增加了元素60之后, slice[3] 的值现在也被更新成了60.3.2 append之后的切片内部示意图
第二种情况:
// 创建一个新的长度为5的整型切片. slice := []int{10,20,30,40} // 基于上述切片, 创建一个新的长度为2, 容量为4的切片. newSlice := append(slice, 50)
当append操作结束之后, 因为超过了原切片底层数组的容量. 因此go给newSlice建了一个新的数组, 把原数组的数据拷贝到了新的数组, newSlice的数组指针也指向了新的数组.
3.3 append结束后, newSlice 有了新的底层数组
再看一下append函数:
func append(s []T, vs ...T) []T
发现 append的参数是一个可变类型, 也即是再使用append时, 可以添加多个元素.
slice := []int{1,2,3} slice = append(slice, 4,5,6)
append也可以添加一个slice, 不过最后要加上”…“, 表示将第二个slice的全部元素添加到第一个slice里. ```go slice1 := []int{1,2,3} slice2 := []int{4,5,6}
slice3 := append(slice1, slice2…)
创建切片3个索引: 在前面的几个例子, 基于已知切片创建新切片时, 都只用了两个索引(开始,结束), 实际上是可以有三个索引可以使用, 分别表示:
开始
,结束
,容量
. 例子// 创建一个水果切片, 长度跟容量都是5. fruits := []string{"apple", "orange", "plum", "banana", "grape"} // 基于水果slice 创建一个新的切片. slice := fruits[2:3:4]
第三个索引的作用主要是用来限制新切片的容量, 给底层数组提供保护, 避免 图3.2 在append时候,修改了公用底层数组.
新的长度,容量计算公式, 给定slice[i:j:k]
- 长度: j-i
- 容量: k-i
迭代切片: 切片是一个集合, 在go里, 只要数据是个集合, 就可以使用
range
来进行对集合的元素进行迭代. 前章谈到的数组. 跟之后要讲到的map
,channel
都可以通过range 来进行迭代.举个🌰:
// Create a slice of integers. // Contains a length and capacity of 4 elements. slice := []int{10, 20, 30, 40} // Iterate over each element and display each value. for index, value := range slice { fmt.Printf("Index: %d Value: %d\n", index, value) } Output: Index: 0 Value: 10 Index: 1 Value: 20 Index: 2 Value: 30 Index: 3 Value: 40
4.1 range迭代示意图
for i:=0; i < len(array); i++ {…}
循环进行迭代, 这里就不赘述了.
多维切片: 和数组一样, 切片也可以是多维的, 也是通过组合多个切片来构造多维切片.
// Create a slice of a slice of integers. slice := [][]int{{10}, {100, 200}}
在函数之间传递切片: 跟数组不同, 前面已经分析了切片的构成. 由三个数据结构, 一个指针(8个byte), 两个整型(64位上是8个byte). 也即是总共24个byte, 考虑到在函数间传递24个byte是非常快速的, 因此没有必要在函数传递切片的时候, 采用指针类型. 而且函数间值传递, 复制的只是切片数据本身, 底层的数组并没有被复制.
事实上 go 在处理pass by value, 跟 pass by reference 的时候, 处理起来前者更高效.
这里吐槽下
protobuf
, 在声明proto文件之后, 转换成对应的go文件,repeated
的数据类型对应的slice
, 其传递方式用的是指针传递而不是值传递.
Author xlk3099
LastMod 2018-04-14