【Golang】函数的参数传递问题——以切片为例
函数存在于各种编程语言中,是可重用的,用于执行指定任务的代码块。C#
中函数(方法)的参数传递默认的是值传递,还有引用传递和输出传递,其中后两种需要在参数类型前面对应加上ref
、out
限制符;除了主要的值传递与引用传递外,C#
数据类型还分为值类型与引用类型。通过排列组合,C#
在函数的定义到使用就有如下几种情况:
-
值传递值类型
-
引用传递值类型
-
值传递引用类型
-
引用传递引用类型
实际上out输出传递也是通过引用传递参数,实质与ref一致,不赘述。
那么go语言呢?
1.开门见山—只有值传递
go语言也有 值类型 引用类型,但是参数传递只有按值传参。因为所谓的传引用(指针):
- 如果实参是值类型:传递给函数的参数是一个指针,而指针代表的是实参的内存地址;
- 如果实参是引用类型:像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型,默认使用引用类型引用的指针进行传递,而不是引用类型变量本身的指针,到了函数内部,也是使用指针的副本,所以依旧是值传递。
这个跟C#还是有区别的。C#的引用传递,传引用,那就真正的引用传递:
static void Main()
{
int n = 5;
Test(ref x);
}
private static void Test(ref int x)
{
x=10;
}
- n与x的地址都是一样。
引用传递:参数是原变量的指针,引用传递参数和原变量的内存地址相同。但是go不是这样的。请往下看。
2.循序渐进—展示
对于值类型的传递,博主不做任何验证,主要以引用类型slice
切片为例,对go语言函数的参数传递进行测试。
2.1 切片
func main() {
var sliceTest []int
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 0x0 只声明
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 0x14000120018
sliceTest = []int{1, 2, 3, 4} // 赋值
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 底层数组地址([0]的地址) 【0x1400013c020】
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 0x14000120018
fmt.Printf("the addr of slice var itself:%v\n", sliceTest) // [1 2 3 4]
fmt.Printf("the addr of slice var itself:%v\n", &sliceTest) // &[1 2 3 4]
// test1
test1(sliceTest)
fmt.Println(sliceTest) //[999 2 3 4]
}
// 普通切片
func test1(slice []int) {
fmt.Printf("-------start test func1----------\n")
fmt.Printf("the addr of func var itself:%p\n", &slice) //这是在函数中内部的变量,新的变量,所以是重新分配的 0x14000120060
fmt.Printf("the value of func var:%p\n", slice) //值传递,底层数组地址([0]的地址) 【0x1400013c020】
slice[0] = 999
fmt.Println(slice) //[999 2 3 4]
fmt.Printf("-------end test func1----------\n")
}
上述函数,整个参数传递,是传递的切片的指针(底层数组,在切片中索引为0的元素的指针),而函数的参数只是对引用类型的指针进行了拷贝,传递参数和原变量的内存地址不同,没错,这就是值传递。
2.2 切片的指针
func main() {
var sliceTest []int
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 0x0 只声明
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 0x14000120018
sliceTest = []int{1, 2, 3, 4} // 赋值
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 底层数组地址([0]的地址) 【0x1400013c020】
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
fmt.Printf("the addr of slice var itself:%v\n", sliceTest) // [1 2 3 4]
fmt.Printf("the addr of slice var itself:%v\n", &sliceTest) // &[1 2 3 4]
// test1
test1(sliceTest)
fmt.Println(sliceTest) //[999 2 3 4]
// test2
test2(&sliceTest)
fmt.Println(sliceTest) //[888 2 3 4]
}
// 切片指针
func test2(slice *[]int) {
fmt.Printf("-------start test func2----------\n")
fmt.Printf("the addr of func var itself:%p\n", &slice) //这是在函数中内部的变量,新的变量,所以是重新分配的 0x14000134020
fmt.Printf("the value of func var:%p\n", slice) //值传递,传递的是main中的切片变量地址,[0x14000120018]
fmt.Printf("the value of pointer:%p\n", *slice) //指针运算符获得切片变量的值 依然是切片的底层数组地址 【0x1400013c020】
(*slice)[0] = 888
fmt.Println(*slice) //[888 2 3 4]
fmt.Printf("-------end test func2----------\n")
}
上述函数,整个参数传递,是传递的切片变量的指针,函数的参数只是对切片变量的指针进行了拷贝,传递参数和原变量的内存地址仍然不同,没错,这依然是值传递。
2.3 返回切片—内部修改元素
func main() {
var sliceTest []int
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 0x0 只声明
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 0x14000120018
sliceTest = []int{1, 2, 3, 4} // 赋值
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 底层数组地址([0]的地址) 【0x1400013c020】
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
fmt.Printf("the addr of slice var itself:%v\n", sliceTest) // [1 2 3 4]
fmt.Printf("the addr of slice var itself:%v\n", &sliceTest) // &[1 2 3 4]
// test1
test1(sliceTest)
fmt.Println(sliceTest) //[999 2 3 4]
// test2
test2(&sliceTest)
fmt.Println(sliceTest)
// test3
sliceTest = test3(sliceTest)
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 底层数组地址([0]的地址) 【0x1400013c020】
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
fmt.Println(sliceTest) // [777 2 3 4]
}
// 普通切片 返回切片
func test3(slice []int) []int {
fmt.Printf("-------start test func3----------\n")
fmt.Printf("the addr of func var itself:%p\n", &slice) //这是在函数中内部的变量,新的变量,所以是重新分配的
fmt.Printf("the value of func var:%p\n", slice) //值却依然是切片的底层数组地址 【0x1400013c020】
slice[0] = 777
fmt.Println(slice) //[777 2 3 4]
fmt.Printf("-------end test func3----------\n")
return slice
}
上述函数,整个参数传递,依然是值传递,就不赘述这个问题。 我们引入了新的操作:在函数内部对切片按索引进行了修改操作。 这样就影响原切片。
2.4 函数内部操作切片—append
func main() {
var sliceTest []int
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 0x0 只声明
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 0x14000120018
sliceTest = []int{1, 2, 3, 4} // 赋值
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 底层数组地址([0]的地址) 【0x1400013c020】
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
fmt.Printf("the addr of slice var itself:%v\n", sliceTest) // [1 2 3 4]
fmt.Printf("the addr of slice var itself:%v\n", &sliceTest) // &[1 2 3 4]
// test1
test1(sliceTest)
fmt.Println(sliceTest) //[999 2 3 4]
// test2
test2(&sliceTest)
fmt.Println(sliceTest)
// test3
sliceTest = test3(sliceTest)
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 底层数组地址([0]的地址) 【0x1400013c020】
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
fmt.Println(sliceTest) // [777 2 3 4]
// test4
test4(sliceTest)
fmt.Printf("the addr of array to slice:%p\n", sliceTest) //array 地址 0x1400013c020
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 0x14000120018
fmt.Println(sliceTest) // [666 2 3 4]
}
func test4(slice []int) {
fmt.Printf("-------start test func4----------\n")
fmt.Printf("the addr of func var itself:%p\n", &slice) // 这是在函数中内部的变量,新的变量,所以是重新分配的 0x14000120180
fmt.Printf("the value of func var:%p\n", slice) // 值却依然是切片的底层数组地址 【0x1400013c020】
slice[0] = 666
slice = append(slice, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
slice[0] = 555
fmt.Println(slice) // [555 2 3 4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17]
fmt.Printf("the addr of func var itself:%p\n", &slice) /// 这是在函数中内部的变量 不变,0x14000120180
fmt.Printf("the value of func var:%p\n", slice) // 0x1400013e000--变了
fmt.Printf("-------end test func4----------\n")
}
上述函数,整个参数传递,依然是值传递,同样不赘述。 我们引入了新的操作:在函数内部对切片进行append。 切片还是像之前博文【Golang】来几道题以加强切片知识那样,切片append后的长度超越了容量,这时底层数组将会更换,所以append之前的操作会影响原切片,之后便不再影响。
2.5 返回切片—内部append
func main() {
var sliceTest []int
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 0x0 只声明
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 0x14000120018
sliceTest = []int{1, 2, 3, 4} // 赋值
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 底层数组地址([0]的地址) 【0x1400013c020】
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
fmt.Printf("the addr of slice var itself:%v\n", sliceTest) // [1 2 3 4]
fmt.Printf("the addr of slice var itself:%v\n", &sliceTest) // &[1 2 3 4]
// test1
test1(sliceTest)
fmt.Println(sliceTest) //[999 2 3 4]
// test2
test2(&sliceTest)
fmt.Println(sliceTest)
// test3
sliceTest = test3(sliceTest)
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 底层数组地址([0]的地址) 【0x1400013c020】
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
fmt.Println(sliceTest) // [777 2 3 4]
// test5
sliceTest = test5(sliceTest)
fmt.Printf("the addr of array to slice:%p\n", sliceTest) // 底层数组地址([0]的地址) ---{0x1400013e0b0}--
fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
fmt.Println(sliceTest) // [444 2 3 4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17]
}
func test5(slice []int) []int {
fmt.Printf("-------start test func5----------\n")
fmt.Printf("the addr of func var itself:%p\n", &slice) // 这是在函数中内部的变量,新的变量,所以是重新分配的 0x14000120210
fmt.Printf("the value of func var:%p\n", slice) // 【0x1400013c020】
slice[0] = 444
slice = append(slice, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
fmt.Println(slice) // [444 2 3 4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17]
fmt.Printf("the addr of func var itself:%p\n", &slice) // 这是在函数中内部的变量 不变,0x14000120210
fmt.Printf("the value of func var:%p\n", slice) // {0x1400013e0b0}--变了
fmt.Printf("-------end test func5----------\n")
return slice
}
上述函数,整个参数传递,依然是值传递,同样不赘述。 我们同样引入了新的操作:在函数内部对切片进行append。 只是,这次我们函数返回了append后的切片,就是已经更换底层数组的切片。
3.结论
-
Go语言中所有的传参都是值传递(传值)。
- 函数内拷贝的内容是非引用类型(
int
、string
、struct
等这些),这样就在函数中就无法修改原内容数据; - 函数内拷贝的内容是引用类型(指针、
map
、slice
、chan
等这些),这样就可以修改原内容数据。
- 函数内拷贝的内容是非引用类型(
-
是否可以修改原内容数据,和传值、传引用没有必然的关系。
- C#中,传引用肯定是可以修改原内容数据的,但是值传递,传递是引用类型,也可以办到,比如类的实例;
- 同样,Go语言里,虽然只有传值,但是我们也可以修改原内容数据,因为参数是引用类型。
-
参数是切片的函数,一定要当心函数内部的操作对切片影响的程度。
- 切片的底层数组的更换;
- 如果对经过函数执行后的切片,仍有后续操作;
- 总之,建议统一采用函数返回切片的方式;
结语:了解底层原理,才能让我们找到规避问题的方案。
- 原文作者:Garfield
- 原文链接:http://www.randyfield.cn/post/2021-08-02-go-function-param/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。