gocookbook
gocookbook copied to clipboard
切片并非引用类型
切片是引用类型吗
数组需要预先声明长度,有些不灵活,因此在Go
代码中不经常见到它们。但是切片却无处不在。切片是一段数组的描述符,编译期间的切片是 slice
类型的,但是在运行时切片由如下的 SliceHeader
结构体表示,其中 Data
字段是指向底层数组的指针(可以理解成底层数组中存储切片索引0
位置上的元素的内存地址),Len
表示切片的长度,而 Cap
表示切片的容量(最大长度)
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
切片与底层数组的联系可以用下面这张图表示
很多地方提起切片都会说它是引用类型,但是在上面的SliceHeader
结构体类型中我们看到切片的属性里只有Data
是指向底层数组的指针,而长度和容量却不是,这在让我们在平时使用切片时如果稍不注意,尤其是带着在其他语言使用引用类型的思维定式来使用切片时程序不但不会按照预期的运行还会出现一些诡异的现象,我们通过三个例子来看一下。
func main() {
var s []int
for i := 1; i <= 3; i++ {
s = append(s, i)
}
reverse(s)
fmt.Println(s)
}
func reverse(s []int) {
for i, j := 0, len(s) - 1; i < j; i++ {
j = len(s) - (i + 1)
s[i], s[j] = s[j], s[i]
}
}
程序最终的输出结果是:
[3 2 1]
上面的代码段首先构建了一个切片s
,一开始s
的值是[1, 2, 3]
。然后在reverse
函数里对切片进行了反转。在main
函数里打印s
会发现在reverse
函数外也能看到reverse
对切片s
的操作结果。这符合我们对引用类型的理解。
现在我们将反转函数的内部稍微修改一下,在反转切片前往里面先追加一个元素。
func reverse(s []int) {
s = append(s, 999)
for i, j := 0, len(s) - 1; i < j; i++ {
j = len(s) - (i + 1)
s[i], s[j] = s[j], s[i]
}
}
这次程序的输出变成了:
[999 3 2]
1
不见了,导致1
不见的原因是当调用append
时,将创建一个新切片。新切片具有新的 “长度” 属性,该属性不是指针,但Data
属性仍指向同一个底层数组。因此,我们函数内的代码最终会反转切片所引用的底层数组(切片里边是不存储任何数据的),但是函数外原始切片的长度属性还是之前的长度值3
,这就是造成了上面 1 被丢掉的原因。
还有比这个更诡异的情况,我们再把上面的反转函数进一步改造一下,多添加几个元素:
func reverse(s []int) {
s = append(s, 999, 1000, 1001)
for i, j := 0, len(s)-1; i < j; i++ {
j = len(s) - (i + 1)
s[i], s[j] = s[j], s[i]
}
}
这次程序的输出变成了:
[1, 2, 3]
在反转函数内对切片的更改在函数外又看不见了,这隐隐约约让我们感觉,切片并不像其他语言的引用类型那样是按照地址传递的。
如前所述,当我们调用append
时,会创建一个新的切片。在第二个例子中,反转函数里的新切片仍指向同一底层数组,因为数组有足够的容量来添加新元素,因此在函数内对底层数组的更改也能在函数外体现,但是这个例子中,在reverse
函数里向切片添加了三个元素,而此时我们的切片的底层数组没有足够的容量来添加新元素了,于是系统分配了一个新数组,让切片指向该数组。这时函数内外的切片指向的不同的底层数组,所以在函数内对切片做的任何更改都不会再影响我们的初始切片。
上面两个例子的切片对应的底层数组的变化如下:
从上面几个切片的例子的分析我们可以得出结论:切片不是什么引用类型。根据切片头结构里的Data
指针推测,指针也不是引用类型,指针参数也是通过值传递给函数内部的。
其实Go里面是没有引用类型的,函数的指针参数在函数开始执行时同样也会被拷贝给函数内的局部变量