提出疑难
在 Go 的源码库或者其余开源我的项目中,会发现有些函数在须要用到切片入参时,它采纳是指向切片类型的指针,而非切片类型。这里未免会产生疑难:切片底层不就是指针指向底层数组数据吗,为何不间接传递切片,两者有什么区别?
例如,在源码 log 包中,Logger
对象上绑定了 formatHeader
办法,它的入参对象buf
,其类型是*[]byte
,而非[]byte
。
func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {}
有以下例子
func modifySlice(innerSlice []string) {innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {outerSlice := []string{"a", "a"}
modifySlice(outerSlice)
fmt.Print(outerSlice)
}
// 输入如下
[b b]
[b b]
咱们将 modifySlice
函数的入参类型改为指向切片的指针
func modifySlice(innerSlice *[]string) {(*innerSlice)[0] = "b"
(*innerSlice)[1] = "b"
fmt.Println(*innerSlice)
}
func main() {outerSlice := []string{"a", "a"}
modifySlice(&outerSlice)
fmt.Print(outerSlice)
}
// 输入如下
[b b]
[b b]
很好,在下面的例子中,两种函数传参类型失去的后果都一样,仿佛没发现有什么区别。通过指针传递它看起来毫无用处,而且无论如何切片都是通过援用传递的,在两种状况下切片内容都失去了批改。
这印证了咱们一贯的认知:函数内对切片的批改,将会影响到函数外的切片。但,真的是如此吗?
考据与解释
在《你真的懂 string 与[]byte 的转换了吗》一文中,咱们讲过切片的底层构造如下所示。
type slice struct {
array unsafe.Pointer
len int
cap int
}
array
是底层数组的指针,len
示意长度,cap
示意容量。
咱们对上文中的例子,做以下轻微的改变。
func modifySlice(innerSlice []string) {innerSlice = append(innerSlice, "a")
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {outerSlice := []string{"a", "a"}
modifySlice(outerSlice)
fmt.Print(outerSlice)
}
// 输入如下
[b b a]
[a a]
神奇的事件产生了,函数内对切片的批改居然没能对外部切片造成影响?
为了清晰地明确产生了什么,将打印增加更多细节。
func modifySlice(innerSlice []string) {fmt.Printf("%p %v %p\n", &innerSlice, innerSlice, &innerSlice[0])
innerSlice = append(innerSlice, "a")
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Printf("%p %v %p\n", &innerSlice, innerSlice, &innerSlice[0])
}
func main() {outerSlice := []string{"a", "a"}
fmt.Printf("%p %v %p\n", &outerSlice, outerSlice, &outerSlice[0])
modifySlice(outerSlice)
fmt.Printf("%p %v %p\n", &outerSlice, outerSlice, &outerSlice[0])
}
// 输入如下
0xc00000c060 [a a] 0xc00000c080
0xc00000c0c0 [a a] 0xc00000c080
0xc00000c0c0 [b b a] 0xc000022080
0xc00000c060 [a a] 0xc00000c080
在 Go 函数中,函数的参数传递均是值传递。那么,将切片通过参数传递给函数,其实质是复制了 slice
构造体对象,两个 slice
构造体的字段值均相等。失常状况下,因为函数内 slice
构造体的 array
和函数外 slice
构造体的 array
指向的是同一底层数组,所以当对底层数组中的数据做批改时,两者均会受到影响。
然而存在这样的问题:如果指向底层数组的指针被笼罩或者批改(copy、重调配、append 触发扩容),此时函数外部对数据的批改将不再影响到内部的切片,代表长度的 len 和容量 cap 也均不会被批改。
为了让读者更清晰的意识到这一点,将上述过程可视化如下。
能够看到,当切片的长度和容量相等时,产生 append,就会触发切片的扩容。扩容时,会新建一个底层数组,将原有数组中的数据拷贝至新数组,追加的数据也会被置于新数组中。切片的 array 指针指向新底层数组。所以,函数内切片与函数外切片的关联曾经彻底斩断,它的扭转对函数外切片曾经没有任何影响了。
留神,切片扩容并不总是等倍扩容。为了防止读者产生误解,这里对切片扩容准则简略阐明一下(源码位于src/runtime/slice.go
中的 growslice
函数):
切片扩容时,当须要的容量超过原切片容量的两倍时,会间接应用须要的容量作为新容量。否则,当原切片长度小于 1024 时,新切片的容量会间接翻倍。而当原切片的容量大于等于 1024 时,会重复地减少 25%,直到新容量超过所须要的容量。
到此,咱们终于晓得为什么有些函数在用到切片入参时,它须要采纳指向切片类型的指针,而非切片类型。
func modifySlice(innerSlice *[]string) {*innerSlice = append(*innerSlice, "a")
(*innerSlice)[0] = "b"
(*innerSlice)[1] = "b"
fmt.Println(*innerSlice)
}
func main() {outerSlice := []string{"a", "a"}
modifySlice(&outerSlice)
fmt.Print(outerSlice)
}
// 输入如下
[b b a]
[b b a]
请记住,如果你只想批改切片中元素的值,而不会更改切片的容量与指向,则能够按值传递切片,否则你应该思考按指针传递。
例题坚固
为了判断读者是否曾经真正了解上述问题,我将下面的例子做了两个变体,读者敌人们能够自测。
测试一
func modifySlice(innerSlice []string) {innerSlice[0] = "b"
innerSlice = append(innerSlice, "a")
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {outerSlice := []string{"a", "a"}
modifySlice(outerSlice)
fmt.Println(outerSlice)
}
测试二
func modifySlice(innerSlice []string) {innerSlice = append(innerSlice, "a")
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {outerSlice:= make([]string, 0, 3)
outerSlice = append(outerSlice, "a", "a")
modifySlice(outerSlice)
fmt.Println(outerSlice)
}
测试一答案
[b b a]
[b a]
测试二答案
[b b a]
[b b]
你做对了吗?