Go 中常用的切片 slice 数据结构是动态数组,切片长度并不固定,在容量不足的时候会自动扩容。
切片实质上是对一个底层数组的抽象视图,由 Go 运行时维护。在运行时,切片由如下的 SliceHeader
结构体表示,其中 Data
字段是指向底层数组的指针,Len
表示当前切片的长度,而 Cap
表示当前切片的容量,也就是 Data
数组的大小。
type SliceHeader struct {Data uintptrLen intCap int
}
切片作为参数入参
我们知道 slice 是一个指针类型,所以我们可能会习惯性地认为:传递切片,等同于传递指针,函数内部对切片的修改,将会影响到函数外部的切片。这一习惯性认知在大部分情况下都是正确的,如以下代码所示:在 test
函数中修改切片,外部的 TestSlice
受到了影响。
func test(s []string) {for i := 0; i < len(s); i++ {s[i] = "b"}
}func TestSlice(t *testing.T) {s := []string{"a", "b"}fmt.Println(s)test(s)fmt.Println(s)
}
输出:
[a b]
[b b]
我们对上面的代码做一些修改,在调用函数的时候触发切片的扩容机制,然后再看看输出:
func test1(s []string) {s = append(s, "b1")for i := 0; i < len(s); i++ {s[i] = "b"}
}func TestSlice1(t *testing.T) {s := []string{"a", "b"}fmt.Println(s)test1(s)fmt.Println(s)
}
输出:
[a b]
[a b]
我们可以发现,test1
里面对切片的修改并没有完全影响到外部的切片。
原因
在 Go 中,函数参数传递机制为 值拷贝。
将切片做为函数参数传递,实际上是拷贝了 SliceHeader
结构体传入参数,结构体包含了指向底层数组的指针,因此在函数内部修改切片,操作的底层数组是一样的。
但是如果函数内的切片触发了切片扩容(如:使用 append
追加元素),Go 运行时会为切片分配一块新的内存空间并将原切片的所有元素拷贝过去,函数内部切片的底层数组指针指向了 新分配 的内存空间,而函数外部切片底层数组指针仍指向 分配前 的地址空间,由此出现了内外切片不一致的情形。
建议
- 操作不涉及切片容量变化,直接传递切片
- 操作涉及切片容量变化,且需要反馈给调用放,传递切片指针。