提出疑问golang
在Go的源码库或者其余开源项目中,会发现有些函数在须要用到切片入参时,它采用是指向切片类型的指针,而非切片类型。这里未免会产生疑问:切片底层不就是指针指向底层数组数据吗,为什么不直接传递切片,二者有什么区别?web
例如,在源码log包中,Logger
对象上绑定了formatHeader
方法,它的入参对象buf
,其类型是*[]byte
,而非[]byte
。数组
1func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {}
有如下例子微信
1func modifySlice(innerSlice []string) {
2 innerSlice[0] = "b"
3 innerSlice[1] = "b"
4 fmt.Println(innerSlice)
5}
6
7func main() {
8 outerSlice := []string{"a", "a"}
9 modifySlice(outerSlice)
10 fmt.Print(outerSlice)
11}
12
13// 输出以下
14[b b]
15[b b]
咱们将modifySlice
函数的入参类型改成指向切片的指针app
1func modifySlice(innerSlice *[]string) {
2 (*innerSlice)[0] = "b"
3 (*innerSlice)[1] = "b"
4 fmt.Println(*innerSlice)
5}
6
7func main() {
8 outerSlice := []string{"a", "a"}
9 modifySlice(&outerSlice)
10 fmt.Print(outerSlice)
11}
12
13// 输出以下
14[b b]
15[b b]
在上面的例子中,两种函数传参类型获得的结果都同样,彷佛没发现有什么区别。经过指针传递它看起来毫无用处,并且不管如何切片都是经过引用传递的,在两种状况下切片内容都获得了修改。函数
这印证了咱们一向的认知:函数内对切片的修改,将会影响到函数外的切片。但,真的是如此吗?工具
考证与解释学习
在《你真的懂string与[]byte的转换了吗》一文中,咱们讲过切片的底层结构以下所示。测试
1type slice struct {
2 array unsafe.Pointer
3 len int
4 cap int
5}
array
是底层数组的指针,len
表示长度,cap
表示容量。ui
咱们对上文中的例子,作如下细微的改动。
1func modifySlice(innerSlice []string) {
2 innerSlice = append(innerSlice, "a")
3 innerSlice[0] = "b"
4 innerSlice[1] = "b"
5 fmt.Println(innerSlice)
6}
7
8func main() {
9 outerSlice := []string{"a", "a"}
10 modifySlice(outerSlice)
11 fmt.Print(outerSlice)
12}
13
14// 输出以下
15[b b a]
16[a a]
神奇的事情发生了,函数内对切片的修改居然没能对外部切片形成影响?
为了清晰地明白发生了什么,将打印添加更多细节。
1func modifySlice(innerSlice []string) {
2 fmt.Printf("%p %v %p\n", &innerSlice, innerSlice, &innerSlice[0])
3 innerSlice = append(innerSlice, "a")
4 innerSlice[0] = "b"
5 innerSlice[1] = "b"
6 fmt.Printf("%p %v %p\n", &innerSlice, innerSlice, &innerSlice[0])
7}
8
9func main() {
10 outerSlice := []string{"a", "a"}
11 fmt.Printf("%p %v %p\n", &outerSlice, outerSlice, &outerSlice[0])
12 modifySlice(outerSlice)
13 fmt.Printf("%p %v %p\n", &outerSlice, outerSlice, &outerSlice[0])
14}
15
16// 输出以下
170xc00000c060 [a a] 0xc00000c080
180xc00000c0c0 [a a] 0xc00000c080
190xc00000c0c0 [b b a] 0xc000022080
200xc00000c060 [a a] 0xc00000c080
在Go函数中,函数的参数传递均是值传递。那么,将切片经过参数传递给函数,其实质是复制了slice
结构体对象,两个slice
结构体的字段值均相等。正常状况下,因为函数内slice
结构体的array
和函数外slice
结构体的array
指向的是同一底层数组,因此当对底层数组中的数据作修改时,二者均会受到影响。
可是存在这样的问题:若是指向底层数组的指针被覆盖或者修改(copy、重分配、append触发扩容),此时函数内部对数据的修改将再也不影响到外部的切片,表明长度的len和容量cap也均不会被修改。
为了让读者更清晰的认识到这一点,将上述过程可视化以下。
能够看到,当切片的长度和容量相等时,发生append,就会触发切片的扩容。扩容时,会新建一个底层数组,将原有数组中的数据拷贝至新数组,追加的数据也会被置于新数组中。切片的array指针指向新底层数组。因此,函数内切片与函数外切片的关联已经完全斩断,它的改变对函数外切片已经没有任何影响了。
注意,切片扩容并不老是等倍扩容。为了不读者产生误解,这里对切片扩容原则简单说明一下(源码位于src/runtime/slice.go
中的 growslice
函数):
切片扩容时,当须要的容量超过原切片容量的两倍时,会直接使用须要的容量做为新容量。不然,当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增长25%,直到新容量超过所须要的容量。
到此,咱们终于知道为何有些函数在用到切片入参时,它须要采用指向切片类型的指针,而非切片类型。
1func modifySlice(innerSlice *[]string) {
2 *innerSlice = append(*innerSlice, "a")
3 (*innerSlice)[0] = "b"
4 (*innerSlice)[1] = "b"
5 fmt.Println(*innerSlice)
6}
7
8func main() {
9 outerSlice := []string{"a", "a"}
10 modifySlice(&outerSlice)
11 fmt.Print(outerSlice)
12}
13
14// 输出以下
15[b b a]
16[b b a]
请记住,若是你只想修改切片中元素的值,而不会更改切片的容量与指向,则能够按值传递切片,不然你应该考虑按指针传递。
例题巩固
为了判断读者是否已经真正理解上述问题,我将上面的例子作了两个变体,读者朋友们能够自测。
测试一
1func modifySlice(innerSlice []string) {
2 innerSlice[0] = "b"
3 innerSlice = append(innerSlice, "a")
4 innerSlice[1] = "b"
5 fmt.Println(innerSlice)
6}
7
8func main() {
9 outerSlice := []string{"a", "a"}
10 modifySlice(outerSlice)
11 fmt.Println(outerSlice)
12}
测试二
1func modifySlice(innerSlice []string) {
2 innerSlice = append(innerSlice, "a")
3 innerSlice[0] = "b"
4 innerSlice[1] = "b"
5 fmt.Println(innerSlice)
6}
7
8func main() {
9 outerSlice:= make([]string, 0, 3)
10 outerSlice = append(outerSlice, "a", "a")
11 modifySlice(outerSlice)
12 fmt.Println(outerSlice)
13}
测试一答案
1[b b a]
2[b a]
测试二答案
1[b b a]
2[b b]
你作对了吗?
往期推荐
Golang技术分享
长按识别二维码关注咱们
更多golang学习资料
回复关键词1024

本文分享自微信公众号 - Golang技术分享(gh_1ac13c0742b7)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。