深入理解GO Slice切片


写在前面

周日下午在家学习,看到一个关于切片的问题,在网上找了一些资料,做个总结。

上代码

func main() {
	sl := make([]int, 0, 10)
	var appenFunc = func(s []int) {
		s = append(s, 10, 20, 30)
		fmt.Println(s, len(sl), cap(sl))
	}
	fmt.Println(sl, len(sl), cap(sl))
	appenFunc(sl)
	fmt.Println(sl, len(sl), cap(sl))
	fmt.Println(sl[:10], len(sl), cap(sl))

	fmt.Println(sl[:], len(sl), cap(sl))
}

你觉得会输出什么?思考一下再往下看。

有的人觉得可能是

[] 0 10
[10 20 30] 3 10  
[] 0 10
[] 0 10
[] 0 10

实际结果是

[] 0 10
[10 20 30] 0 10
[] 0 10
[10 20 30 0 0 0 0 0 0 0] 0 10
[] 0 10

是不是差别很大?这里的差别主要是第四行、第五行的结果。

第一行:比较好理解,切片没有做任何修改,值应该是:[] 0 10

第二行:也很好理解,在appendFunc函数中打印sl,结果是:[10 20 30] 3 10
第三行:由于go都是值传递,所以传到appendFunc函数中的sl其实是切片复制了一份,对原sl没有影响,所以输出结果是:[] 0 10

第四行:sl[:10]这个应该会报数组越界错误才对?怎么没有报错,而且还输出了10 20 30,但是len(sl)等于0,很奇怪

第五行:如果第四行能输出整个数组的内容,这里的sl[:]应该也能输出内容,但是为什么是空的?

分析原因

这里有2个问题:

  • 为什么sl[0:10]能输出10个元素,并且打印出了函数中添加的元素?但是len(sl)等于0
  • 为什么sl[:]输出空数组?

大家都知道,slice(切片)的底层实现,slice 底层存储的数据结构指向了一个 array(数组),如下图:

upload successful


slice结构体定义如下

type SliceHeader struct {  
    Data uintptr  
    Len  int  
    Cap  int  
}
  • Data:指向具体的底层数组。
  • Len:代表切片的长度。
  • Cap:代表切片的容量。

核心要记住的是:slice 真正存储数据的地方,是一个数组。slice 的结构中存储的是指向所引用的数组指针地址

看到这里你应该明白了,传入到appendFunc函数的sl虽然是外层定义的sl的一个值拷贝,它的修改不会影响原sl的内容,但是由于Data是个指针,appendFunc函数对Data的修改自然就影响了原sl的Data,这个很好理解。

要记住一个关键点:如果传过去的值是指向内存空间的地址,是可以对这块内存空间做修改的

对于第一个问题:为什么sl[0:10]能输出10个元素?

这里跟切片访问的一个优化有关,当用s[low : high]访问切片的时候,表达式 s[low : high] 中的 high,最大的取值范围对应着切片的容量(cap),不是单纯的长度(len) 。因此调用 fmt.Println(sl[:10]) 时可以输出容量范围内的值,不会出现越界。

相对的 fmt.Println(sl), fmt.Println(sl[:]) 因为该切片 len 值为 0,没有指定最大索引值,high 则取 len 值,导致输出结果为空。

总结

本文主要涉及到切片在函数中的值传递和修改对原切片的影响,但是并没有考虑切片扩展的问题。这里有一篇文章专门介绍了一下扩容的情况下对原数组的影响,可以看看Go Slice底层实现

参考

[1]Go Slice底层实现

[2]Go 切片这道题,吵了一个下午!


文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录