这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战
这道题你做对了吗?
package main
import "fmt"
func main() {
var s []int
for i := 0; i < 3; i++ {
s = append(s, i)
}
modifySlice(s)
fmt.Println(s)
}
func modifySlice(s []int) {
s = append(s, 2048)
s[0] = 1024
}
猜猜看这段代码输出的是什么?
正确答案是[1024 1 2]
这道题主要考了对Go传参值传递的理解和slice引用类型的理解,我们先来看一下值传递的定义。
值传递
值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
go中所有的传参都是值传递
因为是值传递,所以main中的s和modifySlice中的参数s,不是同一个s,我们可以通过打印地址来验证一下
package main
import "fmt"
func main() {
var s []int
for i := 0; i < 3; i++ {
s = append(s, i)
}
fmt.Printf("main中s的地址: %p\n", &s)
modifySlice(s)
fmt.Println(s)
}
func modifySlice(s []int) {
fmt.Printf("modifySlice中s的地址: %p\n", &s)
s = append(s, 2048)
s[0] = 1024
}
//打印结果
main中s的地址: 0xc000096060
modifySlice中s的地址: 0xc000096078
地址是不一样的,果然是值复制,但既然说不会影响到实际参数,那么为什么在modifySlice中s[0] = 1024却影响到了打印结果呢?我们看一下slice的底层结构
slice的底层结构
//runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice本质就是一个结构体,他的第一个元素是指针类型,这个指针指向底层数组的第一个元素。所以在进行值复制的时候,新的slice中的指针,还是指向相同的底层数组,说白了复制的只是指针而已。所以在执行s[0] = 1024时,指向的底层数组发生了改变。
你真的会了吗
我们对之前的代码略做改动
package main
import "fmt"
func main() {
var s []int
for i := 0; i < 3; i++ {
s = append(s, i)
}
modifySlice(s)
fmt.Println(s)
}
func modifySlice(s []int) {
s = append(s, 2048)
s = append(s, 4096)
s[0] = 1024
}
猜猜看打印结果是什么?还是1024 1 2吗?
答案是否定的,实际结果是0 1 2,即然复制的是指针,s[0] = 1024不是修改了底层数组吗?为什么又不生效了呢?这个就跟slice grow有关了。
slice grow
触发条件
在刚刚slice的底层结构中,我们有看到cap这个元素,即slice的容量,在要放入的元素超过slice容量时,slice会发生扩容,也就是slice grow。那么slice grow实际究竟执行了什么逻辑呢?
扩容规则
我们先来看一下slice grow的源码
func growslice(et *_type, old slice, cap int) slice {
// ……
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for newcap < cap {
newcap += newcap / 4
}
}
}
// ……
//内存对齐操作
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
}
-
slice grow会创建一个新的底层数组
-
如果新旧slice容量之和大于旧容量*2,则新slice容量为新旧之和
-
如果小于 且 长度小于1024,则扩容为2倍
-
如果小于 且 长度大于1024,则扩容为1.25倍
-
在以上操作后,还要进行内存对齐(之前计算后的结果,不是最终结果)
为啥搞这么麻烦,说白了,为了节省珍贵的内存资源呗
答案解析
在modifySlice中,s的容量为4,在append4096后,发生了扩容,底层数组发生了复制,所以执行s[0] = 1024时左右到了新的底层数组上,main中的s不会执行旧的底层数组,不会受影响,打印结果为[0 1 2]
总结
slice的两个特性
共享
slice允许多个slice执行同一个底层数组。在很多场景下都能通过这个特性实现 no copy 而提高效率。但共享同时意味着不安全。使用时需要小心!
go对no copy是真的执着呀
扩容
并不是粗暴的小于1024翻倍,大于1024翻1.25呗,还有其它的判断,尤其最后还有个内存对齐需要注意