盘点Go中使用slice切片容易出错的地方

74 阅读3分钟

最近在准备面试,看了一些Go关于slice的问题,从Go的slice函数传参到slice的扩容等问题,在这里总结一下

场景预设1

我们需要设计一个change函数,这个函数需要传入一个int类型的切片,然后将该切片的所有元素的值修改为0
我们也许会这样设计

func main() {
	arr := []int{1, 2, 3}
	change(arr)
	fmt.Println(arr)
}

func change(arr []int) {
	for i := 0; i < len(arr); i++ {
		arr[i] = 0
	}
}

运行结果

image.png

可以看到,在这种情况下是可以在函数里面直接修改传入的切片的

Go的函数传参

为什么呢?
这就涉及Go的函数传参方式了,众所周知,Go的函数传参,只有值传递这一种,但对于slice切片来说,其实在函数传参的时候,传递的是指针

场景预设2

此时我们的设计需求发生了变换,在原来的change函数,我们希望切片所有元素值修改成0后,在后面添上一个1
此时代码只需增加一个append

func main() {
	arr := []int{1, 2, 3}
	change(arr)
	fmt.Println(arr)
}

func change(arr []int) {
	for i := 0; i < len(arr); i++ {
		arr[i] = 0
	}
	arr = append(arr, 1)
}

运行结果如下

image.png

可以看到原数组没有后面没有添加1
为什么?
我们尝试打印一下arr在不同阶段的地址

func main() {
	arr := []int{1, 2, 3}
	fmt.Printf("执行change前 arr地址%p\n", arr)
	change(arr)
	fmt.Println(arr)
}

func change(arr []int) {
	for i := 0; i < len(arr); i++ {
		arr[i] = 0
	}
	fmt.Printf("执行append前 arr地址%p\n", arr)
	arr = append(arr, 1)
	fmt.Printf("执行append后 arr地址%p\n", arr)
}

image.png

可以看到append后arr地址发生改变,但是返回main函数打印arr地址又变回来了

slice的扩容机制

原因就在于append在扩容的时候,如果空间不够,会将扩容后的数组,放到新的空间里面,但是我们在main函数使用的还是原来那片地址空间的数组,所以不会发生改变

所以,是不是我们提前使用make就可以避免了?

func main() {
	arr := make([]int, 0, 10)
	arr = append(arr, []int{1, 2, 3}...)
	fmt.Printf("执行change前 arr地址%p\n", arr)
	change(arr)
	fmt.Printf("arr长度%d 容量%d\n", len(arr), cap(arr))
	fmt.Printf("执行change后 arr地址%p\n", arr)
	fmt.Println(arr)
}

func change(arr []int) {
	for i := 0; i < len(arr); i++ {
		arr[i] = 0
	}
	fmt.Printf("执行append前 arr地址%p\n", arr)
	arr = append(arr, 1)
	fmt.Printf("arr长度%d 容量%d\n", len(arr), cap(arr))
	fmt.Printf("执行append后 arr地址%p\n", arr)
}

执行结果

image.png

可以看到arr地址没有发生改变,但是仍然没有成功扩容
这里问题可以参考Go Slice 扩容的这些坑你踩过吗?

Slice的底层结构

这个问题我们需要先看下slice的底层结构是怎样的

//runtime/slice.go
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

可以看到实际是一个结构体,结合前面函数传参是值传递的情况,arr相关的结构体也是值传递,也就是实际main函数的slice切片和change函数的slice切片所使用的储存切片的结构体是不同一个的,但是他们的指针是指向同一个数组空间