切片,对于很多 Golang 开发者来说肯定不陌生,在日常开发或是一些框架的源码中都被大量应用。利用切片的相关操作可以实现很多的数据结构,栈、队列、堆等。题外话,笔者觉得 Java 在开发便利性方面要比 Golang 好,像上面提到的堆,Java 有封装好的 class 直接 new,而 golang 却需要实现两个包中同名接口共 5 个方法才行。
Golang
type heap []pair // 视情况创建和业务有关的结构体
// 下面三个方法都是 sort 包中的 Interface 接口需要实现的方
// 堆的大小
func (h heap) Len() int { return len(h) }
// Less 返回 bool 类型。i 位置是新加入元素未调整前的位置,j 位置是父节点元素的位置
func (h heap) Less(i, j int) bool {}
// 交换位置
func (h heap) Swap(i, j int) {}
// 下面两个方法都是 heap 包中的 Interface 接口需要实现的方法
// 添加元素
func (h *heap) Push(v interface{}) {}
// 弹出元素
func (h *heap) Pop() interface{} {}
以上可以看出,Golang 的堆实现较麻烦且需要开发者对切片和指针有清晰的了解才能正确实现。
从 22 年开始学习 Golang 知晓切片到现在,每当感觉就这点东西时,切片又让我"重修",这也是为啥文章标题取名常看常新的原因。近日,在刷题时看到下面一段切片的使用代码,产生了点小疑惑:
Golang
ans := [][]int{}
comb := []int{}
var dfs func(target, idx int)
dfs = func(target, idx int) {
if idx == len(candidates) {
return
}
if target == 0 {
ans = append(ans, append([]int(nil), comb...)) // 疑惑的地方
return
}
dfs(target, idx+1)
if target-candidates[idx] >= 0 {
comb = append(comb, candidates[idx])
dfs(target-candidates[idx], idx)
comb = comb[:len(comb)-1]
}
}
dfs(target, 0)
ans 是个切片,里面装的也是个切片,为啥不直接 append,而是将 comb 中的元素倒出来放到另一个新的切片中,再添加 ?看(tuo)来(ku)有(zi)内(fang)幕(pi)。于是写代码测试了下,发现了端倪。
Golang
slice := make([][]int,1)
slice1 := []int{1,2}
slice = append(slice,slice1)
fmt.Println("slice1 ",slice1)
fmt.Println("slice ",slice)
fmt.Println(" ------- ")
slice1 = slice1[:len(slice1)-1]
fmt.Println("slice1 ",slice1) // ①
fmt.Println("slice ",slice) // ②
fmt.Println(" -------- ")
slice1 = append(slice1,3) // -- 3
// slice1 = append(slice1,3,4) // -- 4
fmt.Println("slice1 ",slice1) // ⑤
fmt.Println("slice ",slice) // ⑥
各位读者可以先想下上面的输出结果是怎样的,然后具体执行看下是否和自己想的一致。有啥说啥,具体的输出确实出乎我意料,以下是放开注释 3,打开注释 4 的输出:
下面是打开注释 3,放开注释 4 的输出:
反常的输出激起了探索欲,此后便开始查各种资料和调试,最终有了能说服自己且符合结果的解释,在这和大家分享下。
Golang 的切片可以看成是由三个属性组成的一个结构体,分别是切片首元素地址 addr ;切片长度 len ;最后是切片容量 cap :
知道了切片结构,还需要知道 slice = append(slice,slice1) 这句在内存中发生了什么 ?继续看图:
紧接着是 slice1 = slice1[:len(slice1)-1] 这行代码,easy,但各位是否清楚这行代码是如何实现的 ?切片由三部分组成,那我们可以在前后分别打印出 addr、len、cap 的值,看下是哪个发生变化不就行了,结果是只有 len 从 2 变成了 1。情况很明朗了,打印时从 addr 开始读 len 个元素,len 变成了 1,所以 ① 处只打印 1,这相当于是把原本放置 2 的地方给让了出来(这有助于理解后面的打印)。 虽然 slice1 的 len 变了,虽然 slice1 在修改前被装入到 slice 这个大容器中,但 slice 中装的是 slice1 的拷贝,结合上面我画的图,也就是 slice 中装的切片 slice2 的 len 属性依旧是 2。 理解了这个也就能明白 ② 处的打印了。
歇一会,每次记录都会是人生的 checkpoint
slice1 = append(slice1,3) ,单看这行代码顶多出在选择题中,但和前面的 slice1 = slice1[:len(slice1)-1] 放一起,或许就是让你等通知和跟你聊薪资要求的分水岭了。怎么分析 ?老样子,把 slice1 的 addr、len、cap 三个值打印出来前后对比下就完了,结果仍然是只有 len 发生了变化,从 1 又变回了 2,addr 没变。 怎么跟之前了解到的需要申请新内存然后拷贝元素不一样了 !原因就是受到了 slice1 = slice1[:len(slice1)-1] 的影响,3 放在了 2 的位置上(能复用就复用,毕竟申请内存和拷贝都是重活)。在这个过程中,slice 是不会发生任何变化的,看图:
有了上图的辅助,明白 ⑤、⑥ 处的打印也顺理成章了。
打开注释 3,放开注释 4 的 slice 的输出缘何没有变化 ?道理很简单,因为 slice1 = slice1[:len(slice1)-1] 只让出了一个位置,而 slice1 = append(slice1,3,4) 却要往里塞两个,自然力不从心,也就只好申请内存拷贝元素了,而 slice 中的切片还是指向原来的地址:
对 Golang 开发者来说,切片是一个十分重要的概念,不仅要学会如何用,还要知道底层原理是啥,不然一不小心切片中多了元素、少了元素,都可能酿成损失。如果有读者有更简洁、易懂的理解方法,欢迎在评论区中提出,大家一块进步。
好耐都冇人问过我开唔开心