Go参数传递
背景
在项目开发中,在使用指针作为函数参数传递时,没有得到想要的结果,所以写篇文章总结一下。
Go语言中的变量可以分为值类型和引用类型,那么当引用类型的变量作为函数参数时是引用传递么?
参数传递类型
在此之前有必要了解一下一般语言的参数传递类型。
值传递
在函数调用时,在系统创建的函数栈帧内部为形参分配一片地址空间,实参将其值拷贝一份存放到该地址空间上。因为形参和实参是不同的内存空间,所以对形参的任何修改不会影响到实参。
地址(指针)传递
在函数调用时,在系统创建的函数栈帧内部为形参分配一片地址空间,地址空间存放的值为实参变量的地址值。虽然形参和实参仍然是不同的内存空间,但因为形参的值是实参的地址值,所以我们可以通过修改形参值所指向的内容来对实参进行修改。但是,请注意,对形参本身的修改是影响不到实参的!(我得不到想要的结果的主要原因在这)
引用传递
引用传递的形参,其实就是实参的一个别名,函数栈帧内部不会为其分配新的地址空间。对引用传递的形参做的任何修改都将影响到实参。
因为go语言里没有引用传递,所以用一个C++的一个例子进行说明吧。
#include<iostream>
using namespace std;
int num = 100;
//值传递
void PassByValue(int value){
cout<<"值传递--形参地址"<<&value<<endl;
value = num;
}
//指针传递1,修改指针指向的值
void PassByPoint1(int *n){
cout<<"指针传递1--形参地址 "<<&n<<endl;
*n=num;
}
//指针传递2,修改指针本身
void PassByPoint2(int *n){
cout<<"指针传递2--形参地址 "<<&n<<endl;
n = #
}
//引用传递, 修改引用变量的值
void PassByReference(int & ref){
cout<<"引用传递--形参地址"<<&ref<<endl;
ref = num;
}
int main() {
int n=10;
cout<<"n 的地址: "<< &n << ", n的值: " << n << endl;
PassByValue(n);
cout<<"值传递修改后, n的地址: "<< &n << ", n的值: " << n <<endl;
n = 10;
cout<<"n 的地址: "<< &n << ", n的值: " << n << endl;
PassByPoint1(&n);
cout<<"指针传递,修改指针指向的值后, n的地址: "<< &n << ", n的值: " << n << endl;
n = 10;
cout<<"n 的地址: "<< &n << ", n的值: " << n << endl;
PassByPoint2(&n);
cout<<"指针传递,修改指针本身后, n的地址: "<< &n << ", n的值: " << n << endl;
n = 10;
cout<<"n 的地址: "<< &n << ", n的值: " << n << endl;
PassByReference(n);
cout<<"引用传递修改后,n的地址值: " << &n << ", n的值: " << n << endl;
return 0;
}
运行结果:
n 的地址: 0x7ffee1e686c8, n的值: 10
值传递--形参地址0x7ffee1e6866c
值传递修改后, n的地址: 0x7ffee1e686c8, n的值: 10
n 的地址: 0x7ffee1e686c8, n的值: 10
指针传递1--形参地址 0x7ffee1e68668
指针传递,修改指针指向的值后, n的地址: 0x7ffee1e686c8, n的值: 100
n 的地址: 0x7ffee1e686c8, n的值: 10
指针传递2--形参地址 0x7ffee1e68668
指针传递,修改指针本身后, n的地址: 0x7ffee1e686c8, n的值: 10
n 的地址: 0x7ffee1e686c8, n的值: 10
引用传递--形参地址0x7ffee1e686c8
引用传递修改后,n的地址值: 0x7ffee1e686c8, n的值: 100
首先从修改后n的值来看,指针传递修改指针指向的内容和引用传递可以影响到实参n
的值,而指针传递修改指针本身和值传递不能。
然后从函数内部形参的地址值来看,值传递和指针传递都开辟了新的内存地址,而引用传递没有。 从这个角度来说,指针传递算是值传递的一种,所以很多也把参数传递分为值传递和引用传递。
Go 参数传递
在go语言中没有传引用的概念,一切参数的传递都是基于值传递。 直接上例子:
package main
import "fmt"
func PassValue(value int) {
fmt.Println("Pass Value,形参地址: ", &value)
value = 100
}
func PassSlice1(s []int) {
fmt.Printf("Pass Slice1,形参地址: %p\n", &s)
s[0] = 100
}
func PassSlice2(s []int) {
fmt.Printf("Pass Slice2,形参地址: %p\n", &s)
s1 := []int{4, 5, 6}
s = s1
}
func PassPoint1(p *int) {
fmt.Println("Pass Point1,形参地址: ", &p)
n := 100
p = &n
}
func PassPoint2(p *int) {
fmt.Println("Pass Point2,形参地址: ", &p)
*p = 100
}
func main() {
n := 0
fmt.Println("实参n的地址:", &n, ", 实参n的值:", n)
PassValue(n)
fmt.Println("PassValue后,实参n的地址:", &n, ", 实参n的值:", n)
s1 := []int{1, 2, 3}
fmt.Printf("实参s1的地址:%p, 实参s1的值:%v\n", &s1, s1)
PassSlice1(s1)
fmt.Printf("PassSlice1后,实参s1的地址:%p, 实参s1的值:%v\n", &s1, s1)
s2 := []int{1, 2, 3}
fmt.Printf("实参s2的地址:%p, 实参s2的值:%v\n", &s2, s2)
PassSlice2(s2)
fmt.Printf("PassSlice2后,实参s2的地址:%p, 实参s2的值:%v\n", &s2, s2)
p := &n
fmt.Println("实参p的地址:", &p, ", 实参p的值:", p, ", 实参n的值: ", n)
PassPoint1(p)
fmt.Println("PassPoint1后,实参p的地址:", &p, ", 实参p的值:", p, ", 实参n的值: ", n)
fmt.Println("实参p的地址:", &p, ", 实参p的值:", p, ", 实参n的值: ", n)
PassPoint2(p)
fmt.Println("PassPoint2后,实参p的地址:", &p, ", 实参p的值:", p, ", 实参n的值: ", n)
}
运行结果:
实参n的地址: 0xc000088008 , 实参n的值: 0
Pass Value,形参地址: 0xc000088010
PassValue后,实参n的地址: 0xc000088008 , 实参n的值:
实参s1的地址:0xc00007a020, 实参s1的值:[1 2 3]
Pass Slice1,形参地址: 0xc00007a060
PassSlice1后,实参s1的地址:0xc00007a020, 实参s1的值:[100 2 3]
实参s2的地址:0xc00007a0a0, 实参s2的值:[1 2 3]
Pass Slice2,形参地址: 0xc00007a0e0
PassSlice2后,实参s2的地址:0xc00007a0a0, 实参s2的值:[1 2 3]
实参p的地址: 0xc000082020 , 实参p的值: 0xc000088008 , 实参n的值: 0
Pass Point1,形参地址: 0xc000082028
PassPoint1后,实参p的地址: 0xc000082020 , 实参p的值: 0xc000088008 , 实参n的值: 0
实参p的地址: 0xc000082020 , 实参p的值: 0xc000088008 , 实参n的值: 0
Pass Point2,形参地址: 0xc000082030
PassPoint2后,实参p的地址: 0xc000082020 , 实参p的值: 0xc000088008 , 实参n的值: 100
可以看到不管是int
的值类型,或者是切片的引用类型,抑或是指针,其作为函数参数进行传递时,都会在函数内部开辟新的内存地址,本质上都是值传递。
Go
语言中引用类型的变量的内存地址上存放的实际上是实际数据的地址。
例如s := []int{1,2,3}
,变量s的内存地址上存放的是数组[1,2,3]
的地址,变量s
本身相当于指向数组内存空间的一个指针类型。所以,示例中尽管将引用类型进行参数传递,但其本质上仍然是值传递。
拓展
函数内存地址分配
上面说到了函数内部会为形参分配新的地址空间,在Go
语言中,新开辟的地址空间是可以作为函数返回值、并在外部使用的。而这在C/C++
中是行不通的。
在C/C++
中,函数内部新开辟的地址空间是在栈上进行分配的,而当函数返回时,栈上的空间会被系统自动回收。所以当在函数外部使用函数返回的内部地址空间时,一定会发生段错误。
而在Go
语言中。当函数为局部变量或形参分配地址空间时,Go
语言编译器会做一个逃逸分析(escape analysis)
的事情。当发现变量的作用域没有跑出函数范围时,就分配在栈上,反之则必须分配到堆上。这一特性,使得我们可以使用函数返回的内部地址空间。Go
语言声称这样可以释放程序员关于内存的使用限制,更多地让程序员关注与程序功能逻辑本身。