开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 10 天,点击查看活动详情
参数传递
熟悉go语言的朋友们都知道,所有函数参数传递的都是值,也就是一个复制品。那么对于这么多数据类型,我们到底在编程中传值好还是传引用好呢?下面先介绍这两个的区别,加以性能测试对比,最后给出最优选择建议。
传值和传引用
- 传值:直接整个结构拷贝一份进入函数体,函数内修改,外部不可见。
- 传引用:传递的参数是一个指针,只对指针进行复制,函数内修改相当于是同一个数据进行了操作,外部是可见的。
从业务上进行选择
根据业务场景和传值、传引用的特点,选择如下:
- 当函数内数据的修改希望外部可见时,使用传引用
- 函数内数据的修改不希望外部可见时,使用传值
- 注意对于slice和map,无论传值传引用修改对外都会可见,引用传递的是底层结构的指针
一个典型的应用场景就是并发编程中对sync各种变量的传递:
- 如果传值,导复制致锁分类,发生不可预测的错误
- 应该传引用,这样在不同函数里都是操作的同一把锁
- 还可以使用闭包
还有一个场景是结构体的方法定义时写结构体指针还是结构体,我们可以把结构体参数当作函数的一个参数看待:
- 如果传值,相当于函数内用的是一个结构体副本
- 传引用相当于修改结构体本体,对外可见
从性能上分析
传参
数组
函数内对数组的操作主要有取值和赋值,下面针对传参和传引用,分别在函数内无赋值和有赋值场景下的性能测试:
// 数组定义及操作
var array [1000000]int
func getArr(arr [1000000]int) int {
tmp := arr[0]
return tmp
}
func getArrWithPtr(arr *[1000000]int) int {
tmp := arr[0]
return tmp
}
func changeArr(arr [1000000]int) {
arr[100]++
}
func changeArrWithPtr(arr *[1000000]int) {
arr[100]++
}
测试代码:
func Benchmark_change_array(b *testing.B) {
for i := 0; i < b.N; i++ {
changeArr(array)
}
}
func Benchmark_change_array_ptr(b *testing.B) {
for i := 0; i < b.N; i++ {
changeArrWithPtr(&array)
}
}
func Benchmark_copy_array(b *testing.B) {
for i := 0; i < b.N; i++ {
getArr(array)
}
}
func Benchmark_copy_array_ptr(b *testing.B) {
for i := 0; i < b.N; i++ {
getArrWithPtr(&array)
}
}
测试结果:
goos: windows
goarch: amd64
pkg: learn/basic/prof/padding/param/array
cpu: AMD Ryzen 7 6800H with Radeon Graphics
Benchmark_change_array-16 4036 305911 ns/op
Benchmark_change_array_ptr-16 1000000000 0.2327 ns/op
Benchmark_copy_array-16 1000000000 0.2327 ns/op
Benchmark_copy_array_ptr-16 1000000000 0.2311 ns/op
结果表明,除了传值时并对值进行修改需要对整个数组进行复制,其他的都不用。
这看起来有点奇怪,为什么传数组不修改数组的值不会发生整个数组的赋值?
有人说只有参数进行赋值操作才进行复制,也许是go语言的一个优化吧?
slice
类似的,也对传参和传引用和进不进行值的修改进行分别的测试:
var slice = make([]int, 1000000)
func getSli(arr []int) int {
tmp := arr[0]
return tmp
}
func getSliWithPtr(arr *[]int) int {
tmp := (*arr)[0]
return tmp
}
func changeSli(arr []int) {
arr[100]++
}
func changeSliWithPtr(arr *[]int) {
(*arr)[100]++
}
func Benchmark_change_Sli(b *testing.B) {
for i := 0; i < b.N; i++ {
changeSli(slice)
}
}
func Benchmark_change_Sli_ptr(b *testing.B) {
for i := 0; i < b.N; i++ {
changeSliWithPtr(&slice)
}
}
func Benchmark_copy_Sli(b *testing.B) {
for i := 0; i < b.N; i++ {
getSli(slice)
}
}
func Benchmark_copy_Sli_ptr(b *testing.B) {
for i := 0; i < b.N; i++ {
getSliWithPtr(&slice)
}
}
结果如下:
goos: windows
goarch: amd64
pkg: learn/basic/prof/padding/param/array
cpu: AMD Ryzen 7 6800H with Radeon Graphics
Benchmark_change_Sli-16 1000000000 0.4782 ns/op
Benchmark_change_Sli_ptr-16 1000000000 0.4524 ns/op
Benchmark_copy_Sli-16 1000000000 0.4635 ns/op
Benchmark_copy_Sli_ptr-16 1000000000 0.4643 ns/op
整体没什么差别,这个还是很符合预期的,因为slice相当于只有两个值和一个指针的结构体,与传值的一个指针相比几乎没有差别,可以随意使用。
type slice struct {
array unsafe.Pointer
len int
cap int
}
string
对长度100w的字符串进行传值和传引用,因为字符串是不可修改的类型,我们就只对比取值的性能:
var str = "&...&" //长度100w
func getStr(s string) byte {
return s[0]
}
func getStrFromPtr(s *string) byte {
return (*s)[0]
}
func Benchmark_copy_str(b *testing.B) {
for i := 0; i < b.N; i++ {
getStr(str)
}
}
func Benchmark_copy_str_ptr(b *testing.B) {
for i := 0; i < b.N; i++ {
getStrFromPtr(&str)
}
}
结果如下:
goos: windows
goarch: amd64
pkg: learn/basic/prof/padding/param/string
cpu: AMD Ryzen 7 6800H with Radeon Graphics
Benchmark_copy_str-16 1000000000 0.4499 ns/op
Benchmark_copy_str_ptr-16 1000000000 0.4735 ns/op
基本上没有差别,string 类型本质上是一个不可变的字符串。它在内部是一个字节数组,存储字符串的字节数据。
type StringHeader struct {
Data uintptr
Len int
}
map
对map同样进行四种情况的测试:
var m = make(map[int]int)
func Benchmark_init(b *testing.B) {
for i := 0; i < 1000000; i++ {
m[i] = i
}
for i := 0; i < b.N; i++ {
}
}
func getMap(m map[int]int) int {
return m[100]
}
func getMapFromPtr(m *map[int]int) int {
return (*m)[100]
}
func changeMap(m map[int]int) int {
m[100]++
return m[100]
}
func changeMapFromPtr(m *map[int]int) int {
(*m)[100]++
return (*m)[100]
}
func Benchmark_copy_from_map(b *testing.B) {
for i := 0; i < b.N; i++ {
getMap(m)
}
}
func Benchmark_copy_from_map_ptr(b *testing.B) {
for i := 0; i < b.N; i++ {
getMapFromPtr(&m)
}
}
func Benchmark_change_from_map(b *testing.B) {
for i := 0; i < b.N; i++ {
changeMap(m)
}
}
func Benchmark_change_from_map_ptr(b *testing.B) {
for i := 0; i < b.N; i++ {
changeMapFromPtr(&m)
}
}
结果如下:
goos: windows
goarch: amd64
pkg: learn/basic/prof/padding/param/map
cpu: AMD Ryzen 7 6800H with Radeon Graphics
Benchmark_init-16 1000000000 0.3027 ns/op
Benchmark_copy_from_map-16 257586014 4.478 ns/op
Benchmark_copy_from_map_ptr-16 262115811 4.524 ns/op
Benchmark_change_from_map-16 123438636 9.764 ns/op
Benchmark_change_from_map_ptr-16 123952113 9.700 ns/op
也没太多差别
struct
首先测试一个比较大的结构体,其中包含一个比较长的数组:
type Content struct {
Detail [10000]int
}
var myStruct Content
func getStruct(myStruct Content) int {
return myStruct.Detail[0]
}
func getStructFromPtr(myStruct *Content) int {
return myStruct.Detail[0]
}
func changeStruct(myStruct Content) int {
myStruct.Detail[0]++
return myStruct.Detail[0]
}
func changeStructFromPtr(myStruct *Content) int {
myStruct.Detail[0]++
return myStruct.Detail[0]
}
func Benchmark_change_struct(b *testing.B) {
for i := 0; i < b.N; i++ {
changeStruct(myStruct)
}
}
func Benchmark_change_struct_from_ptr(b *testing.B) {
for i := 0; i < b.N; i++ {
changeStructFromPtr(&myStruct)
}
}
func Benchmark_copy_struct(b *testing.B) {
for i := 0; i < b.N; i++ {
getStruct(myStruct)
}
}
func Benchmark_copy_struct_from_ptr(b *testing.B) {
for i := 0; i < b.N; i++ {
getStructFromPtr(&myStruct)
}
}
结果如下:
goos: windows
goarch: amd64
pkg: learn/basic/prof/padding/param/struct
cpu: AMD Ryzen 7 6800H with Radeon Graphics
Benchmark_change_struct-16 941833 1295 ns/op
Benchmark_change_struct_from_ptr-16 1000000000 0.2549 ns/op
Benchmark_copy_struct-16 1000000000 0.2315 ns/op
Benchmark_copy_struct_from_ptr-16 1000000000 0.2323 ns/op
结果数组类似,推荐使用传引用的方式(如果不修改值,也都差不多)
下面使用一个小一点的结构体进行测试,只包含十个数字:
type Content struct {
Detail int
Detail1 int
Detail2 int
Detail3 int
Detail4 int
Detail5 int
Detail6 int
Detail7 int
Detail8 int
}
结果如下:
goos: windows
goarch: amd64
pkg: learn/basic/prof/padding/param/struct2
cpu: AMD Ryzen 7 6800H with Radeon Graphics
Benchmark_change_struct-16 1000000000 1.257 ns/op
Benchmark_change_struct_from_ptr-16 1000000000 0.2322 ns/op
Benchmark_copy_struct-16 1000000000 0.2497 ns/op
Benchmark_copy_struct_from_ptr-16 1000000000 0.2390 ns/op
小的结构体,传值和传引用差距小一些,不过还是传引用更快。
一个特例
刚才的测试发现,对于数组和结构体,即使传值,只要不进行赋值操作,也不会触发,引用。下面举一个反例,当数组存储的包含一个数组的结构体:
const NumOfElems = 1000
type Content struct {
Detail [10000]int
}
var arr [NumOfElems]Content
func withValue(arr [NumOfElems]Content) int {
return 1
}
func withReference(arr *[NumOfElems]Content) int {
return 1
}
func TestFn(t *testing.T) {
var arr [NumOfElems]Content
withValue(arr)
withReference(&arr)
}
func BenchmarkPassingArrayWithValue(b *testing.B) {
for i := 0; i < b.N; i++ {
withValue(arr)
}
}
func BenchmarkPassingArrayWithRef(b *testing.B) {
for i := 0; i < b.N; i++ {
withReference(&arr)
}
}
结果如下:
goos: windows
goarch: amd64
pkg: learn/basic/prof/padding/param/array_struct
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkPassingArrayWithValue-16 70 16068289 ns/op
BenchmarkPassingArrayWithRef-16 1000000000 0.2346 ns/op
Amazing,结论失效了,即使我们什么都没干,也进行了参数复制,消耗了大量的时间。
这是为啥内?
这样子,在不清楚原理的情况下还是尽量传引用吧。
返回值
还有一个容易忽略的点的,函数的返回值应该传值还是传引用?返回值也是对值的复制,但是函数返回了就原函数就不会继续操作了,也就不必担心返回的值修改影不影响原来的函数。但是他们的性能还是有差别的,下面对于数组返回值和引用进行测试:
var array [1000000]int
func returnArr(arr *[1000000]int) [1000000]int {
return *arr
}
func returnArrWithPtr(arr *[1000000]int) *[1000000]int {
return arr
}
var d [1000000]int
var d2 *[1000000]int
func Benchmark_return_array_and_change(b *testing.B) {
for i := 0; i < b.N; i++ {
tmp := returnArr(&array)
tmp[0]++
}
}
func Benchmark_change_array_ptr_and_change(b *testing.B) {
for i := 0; i < b.N; i++ {
tmp := returnArrWithPtr(&array)
tmp[0]++
}
}
func Benchmark_return_array(b *testing.B) {
for i := 0; i < b.N; i++ {
d = returnArr(&array)
}
}
func Benchmark_change_array_ptr(b *testing.B) {
for i := 0; i < b.N; i++ {
d2 = returnArrWithPtr(&array)
}
}
结果如下:
goos: windows
goarch: amd64
pkg: learn/basic/prof/padding/param/return
cpu: AMD Ryzen 7 6800H with Radeon Graphics
Benchmark_return_array_and_change-16 916 1400395 ns/op
Benchmark_change_array_ptr_and_change-16 1000000000 0.2484 ns/op
Benchmark_return_array-16 1014 1198862 ns/op
Benchmark_change_array_ptr-16 1000000000 0.4707 ns/op
结果和传参差不多,不过传值就算我们不再赋值,也会引发赋值。所以返回值传值还是传引用应该和传参数差不多。
还有一个刁钻的场景是返回值传值,但是函数调用没有接收返回值,也是不会参数参数复制的消耗的。因为不接收返回值,那就没必要再返回了,这个情况我测试了,不过没必要写在上面了。
总结
经过上面一系列测试,得到如下结论:
-
首先根据业务场景确定传参还是传引用
-
其次是性能上的
- 基本数据类型、slice、map传参传引用都差不多
- 数组、struct还是建议传引用
-
最后,返回值的类型选取和参数选取方法一致
ps:有大哥知道上面特例的原因可以在评论区补充,谢谢