聊一聊 slice
数组
在开始介绍切片之前需要先介绍一下 go 中的数组。数组是一块连续的存储空间, 定义了存储的类型和长度。下面是是声明长度为 3 的 int 数组, 初始值为 0. 数组可以直接用来比较, 当元素相同时, 返回 ture. 对于数组越界访问, 直接会编译报错.
初始化
var arr [3]int // [0, 0, 0]
arr1 := [...]int{0, 0, 0} // [0, 0, 0]
fmt.Println(a == cc) //比较
// a[3] = 0 Invalid array index '3' (out of bounds for the 3-element array)
传参
当数组作为参数传递时, 传递的是数组的一个 copy, 更新操作并不会修改原数组
func main() {
arr := [3]int{1, 2, 3}
ArrParam(arr)
fmt.Println(arr) // [1 2 3]
}
func ArrParam(arr [3]int) {
arr[0] = 0
}
切片
切片是对数组的封装, 使用切片可以灵活的对数组进行扩容和裁剪。当我们使用数组时, 如果需要对数组进行追加操作, 需要先创建一个新的数组, 同时进行赋值操作。使用切片的话, 只通过一个 append 函数就可以实现追加操作。
切片底层是一个结构体, 里面包含了指向连续内存空间的指针, 已使用的长度, 申请空间的总长度。
初始化
var s []int // 声明一个切片
fmt.Println(s == nil) // true
s1, s2 := []int{1}, []int{1, 2, 3}
//fmt.Println(s1 == s2) Invalid operation: s1 == s2 (the operator == is not defined on []int)
fmt.Println(s1, len(s1), cap(s1)) // [1] 1 1
fmt.Println(s2, len(s2), cap(s2)) // [1 2 3] 3 3
s4 := make([]int, 1)
fmt.Println(s4, len(s4), cap(s4)) //[0] 1 1
s5 := make([]int, 1, 2)
fmt.Println(s5, len(s5), cap(s5)) //[0] 1 2
扩容
在使用 make 声明一个数组时, 我们可以指定要申请的空间大小「cap」。例如:s5 := make([]int, 1, 2),申请了一块容量为 3 的存储空间, 使用的容量为 2, 最后一块空间并没有使用。
s5 := make([]int, 1, 3)
fmt.Println(s5, len(s5), cap(s5)) // [0] 1 3
为什么需要支持显性的指定申请的切片容量呢?
有些业务场景, 需要对数组进行扩容。例如, 我们需要在 s5 后面追加一个 int 3。通过提前申请指定的容量, 可以避免重新申请内存, 从而提升运行速度。下面是一个对数组进行追加的例子, 使用 s5 可以直接修改对应的值, 同时更新 len; 使用 s6 的形式, 需要先申请容量为 3 的内存, 然后进行拷贝和赋值。
s5 := make([]int, 2, 3)
s5 = append(s5, 1)
fmt.Println(s5, len(s5), cap(s5))
s6 := make([]int, 2)
s6 = append(s6, 1)
fmt.Println(s6, len(s6), cap(s6))
使用 append 函数进行数组追加时, 底层会先判断当前切片的容量是否满足, 如果不满足将申请新的存储空间。下面一个例子, 说明了扩容机制对于程序的影响。核心要区分清楚, 数组追加的时候是否需要进行重新申请内存空间。
s := []int{1}
s1 := s[0:1]
s2 := append(s, 1) // s2 指向新的内存空间
s1[0] = 0
fmt.Println(s, s2) // [0], [1,1]
s3 := []int{1, 2}
s3 = append(s3, 1) // s3 分配一段 len:3, cap:4 的内存空间
fmt.Println(s3, len(s3), cap(s3)) // [1 2 1], 3, 4
s4 := s3[0:1]
s5 := append(s3, 1) // 使用现有的内存空间, 更新 len
s4[0] = 0 // s3[0] 也会被修改
fmt.Println(s4, s5) // [0], [0 2 1 1]
赋值
数组和切片是支持通过下标访问的。在 go 语言中, 可以通过 slice[left:right:cap] 的形式访问一段空间, 对应的含义是访问下标从 left - right 的左闭右开的空间, cap 可选, 可以指定容量下标。在切片赋值时, 相当于新建一个 slice, 底层还是共享同一块存储空间, 这样可以减少内存的分配与复制, 但也会有一些坑。例如下面的例子, 更新完 s 之后, s1 会同步被更新
s := []int{1, 2, 3}
s1 := s[0:2] // [1, 2], 与 s 有相同的指向
fmt.Println(s1, len(s1), cap(s1)) // [1, 2], 2, 3
s[0] = 0
fmt.Println(s1) // [0,2]
如果我们期望不使用同一块存储空间, 需要对切片进行单独修改, 可以使用内置的 copy 函数, 显性的对切片进行拷贝
s := []int{1, 2, 3}
s1 := make([]int, 2)
copy(s1, s) // 赋值从 s1 的 0 下标开始, 到 min(len(s1), len(s)) 接口
s[0] = 0 // 不影响 s1 的数据
fmt.Println(s1, s) // [0,2], [0 2 3]
传参
切片作为参数传递时, 其实是值传递。将 slice 看作一个结构体 struct{point, len, cap}, 参数传递时, 传递的是对象的值拷贝。从下面的例子, 我们可以看到, 函数里面进行扩容并不影响原切片「如果函数更改了指向数组的值, 原切片也会受影响」
func main() {
s := []int{1, 2, 3}
SliceParam(s)
fmt.Println(s) // [1 2 3]
s1 := make([]int, 3, 4) // [0 0 0]
SliceParam(s1)
fmt.Println(s1) // [0 0 0]
}
func SliceParam(s []int) {
s = append(s, 1)
}
如果我们想让函数更新传入的切片, 可以使用指针。下面的两个图, 说明了传递指针和传递参数的区别。
func main() {
s := []int{1, 2, 3}
SliceParam(&s)
fmt.Println(s) // [1 2 3 1]
s1 := make([]int, 3, 4) // [0 0 0 1]
SliceParam(&s1)
fmt.Println(s1) // [0 0 0]
}
func SliceParam(s *[]int) {
*s = append(*s, 1)
}
快捷操作
func main() {
s := []int{0, 1, 2, 3, 4, 5}
// 增加
s1 := append(s, []int{6, 7, 8}...)
fmt.Println(s1) // [0 1 2 3 4 5 6 7 8]
// 删除
sc := []int{0, 1, 2, 3, 4, 5}
i := 1
s2 := append(sc[:i], sc[i+1:]...)
fmt.Println(s2) // [0 2 3 4 5]
// 插入
scc := []int{0, 2, 3, 4, 5}
s3 := append(scc[:1], append([]int{1}, scc[1:]...)...)
fmt.Println(s3) // [0 1 2 3 4 5]
// 反转
sccc := []int{0, 1, 2, 3, 4, 5}
for left, right := 0, len(sccc)-1; left < right; left, right = left+1, right-1 {
sccc[left], sccc[right] = sccc[right], sccc[left]
//r := s[right]
//l := s[left]
//s[left] = r
//s[right] = l
}
fmt.Println(sccc) // [5 4 3 2 1 0]
}
总结
在使用切片时, 我们始终牢记切片本质上就是一个普通的结构体, 里面包含了三个元素「连续内存指针, len, cap」; 出于性能的考虑, 在赋值操作时, 多个切片会共享同一块内存; 当切片触发扩容操作时, 切片指向的连续内存会发生变更。
参考
Go Slices: usage and internals - The Go Programming Language
Arrays, slices (and strings): The mechanics of 'append' - The Go Programming Language
转载自:https://juejin.cn/post/7208823936300695611