第一版代码
package main
import "fmt"
func main() {
list := make([]int, 0)
testList(list)
fmt.Println(list)
}
func testList(list []int) {
for i := 0; i < 10; i++ {
list = append(list, i)
}
}
先看第一段代码,在很直观的理解中,main 函数中的打印应该是 0 到 9 的一串数字。 但是我们运行之后就会发现打印的结果是空。
现在来解释一下上面的代码为啥错误。
-
main 函数中使用 make 初始化的切片的长度和容量都是 0,所以在 testList 函数中 append 的时候就会发生扩容,扩容时候就会重新申请一片内存空间。然后切片重新指向这片新的内存空间。
-
golang 中函数的穿参都是值传递。意思就是 testList 内的 list 参数其实是 main 函数中 list 的副本。
基于以上两点,我们就发现了。由于 append 的扩容为切片重新申请了一个新的内存空间。testList 函数修改了副本 list 内的指针指向。但是 main 函数内的并没有修改,指向的仍然是旧的数组地址。
所以最终的打印结果是空。
第二版代码
package main
import "fmt"
func main() {
list := make([]int, 0, 100)
testList(list)
fmt.Println(list)
}
func testList(list []int) {
for i := 0; i < 10; i++ {
list = append(list, i)
}
}
基于第一版中的错误,我初始化的时候将切片容量直接设置为 100,循环 10 次 append 肯定不会扩容了。
再次运行输出结果还是为空。
现在来解释一下第二版的代码为啥还是错误。
虽然不会扩容了。main 函数中的 list 与 testList 函数中的 list 虽然都指向原始数组。且 testList 函数也确实修改了原始数组。
但是,上方代码并没有解决值传递副本的问题。
切片的结构体为
type slice struct {
array unsafe.Pointer // 原始数组指针。数据存储在这个指针指向的内存中
len int
cap int
}
所以在函数传递参数为切片时,会对整个切片进行复制,所以复制的不仅仅是指针,还有 len 和 cap。
在 testList 函数中 append 是对副本进行了修改。main 函数中的 list 虽然指针仍然指向原始数组,但是 len 和 cap 并没有修改。
所以打印的时候仍然为空。
原始数组修改验证
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
list := make([]int, 0, 100)
testList(list)
// 直接打印切片内容
fmt.Println(list)
// 获取切片头的指针
slicePtr := (*reflect.SliceHeader)(unsafe.Pointer(&list))
fmt.Println(slicePtr)
// 通过切片指针获取,原始数组
arrayPtr := (*[10]int)(unsafe.Pointer(slicePtr.Data))
// 打印数组内容
fmt.Println(*arrayPtr)
}
func testList(list []int) {
// 获取切片头的指针
slicePtr := (*reflect.SliceHeader)(unsafe.Pointer(&list))
fmt.Println(slicePtr)
for i := 0; i < 10; i++ {
list = append(list, i)
}
}
以上代码直接打印切片内容为空。通过 unsafe 包接口获取到的原始数组内容则是 [0 1 2 3 4 5 6 7 8 9]。 由此可以证明 testList 函数确实修改了原始数组,但是由于值传递的参数复制。testList 函数内部修改的是 list 的副本,并未修改 main 函数中的 list。
还有就是 testList 函数打印 slicePtr 与 main 函数中打印 slicePtr 的,原始数组指针一致。也可以证明。
正确修改第一版
package main
import "fmt"
func main() {
list := make([]int, 0)
list = testList(list)
fmt.Println(list)
}
func testList(list []int) []int {
for i := 0; i < 10; i++ {
list = append(list, i)
}
return list
}
testList 函数修改完成之后,将 list 返回,main 函数重新赋值。
正确修改第二版
package main
import "fmt"
func main() {
list := make([]int, 100)
testList(list)
fmt.Println(list)
}
func testList(list []int) {
for i := 0; i < 10; i++ {
list[i] = i
}
}
初始化时,设置好容量。然后 testList 不再使用 append 而是直接使用索引进行修改。
正确修改第三版
package main
import "fmt"
func main() {
list := make([]int, 0)
testList(&list)
fmt.Println(list)
}
func testList(list *[]int) {
for i := 0; i < 10; i++ {
*list = append(*list, i)
}
}
testList 函数改为指针参数,即使是值传递,参数发生复制,复制的也仅仅是 main list 的指针。而不是像之前一样需要复制切片的头结构。
对比三个正确版本的代码
- 第一个版本,将结果返回。这个方法的缺点就是 testList 函数的结果需要压入 main 函数栈内。
- 第二个版本,需要初始化的时候设置好容量,否则容易数组越界。
- 第三个版本,使用指针,比较直观且代码比较简单。