Go程序员常常本来是其它某种语言的老鸟,不同语言对引用类型的定义也许不一样。这可能造成误会。本文是为了明确Go语言的引用类型的定义,特别是它与其它语言中引用这一概念的异同。
很多阅读文档不仔细的初学者(包括我),在刚开始写Go的时候,往往写过类似如下的代码:
package main
import "fmt"
func main() {
c := make(chan int)
go f(&c)
fmt.Println(<-c)
}
func f(c *chan int) {
*c <- 1
}
这段代码没有错误,可以正常运行。但阅读文档比较仔细的,或者多写过几天Go的朋友,会更习惯下面的写法:
package main
import "fmt"
func main() {
c := make(chan int)
go f(c) // 不再取指针
fmt.Println(<-c)
}
func f(c chan int) { // 参数不再是chan的指针
c <- 1
}
按照互联网博客的主流说法,这是因为chan属于Go语言中的“引用类型”。这本身没问题,问题在于,直接使用“引用”一词,可能会引起误会。因为Go程序员常常本来是其它某种语言的老鸟,不同语言对引用一词的定义也许不一样。如果以C++标准为参考,Go语言中所谓的“引用类型”,实质上就是指针。因此我们甚至可以说:Go语言中只存在值传递,并不存在引用传递。
我不是在想当然,这可以从源码里轻易得到验证。仍然以上文的chan
为例,我们可以在源码中找到其对应make
的实现:
func makechan(t *chantype, size int) *hchan
可以看到,其返回值的确是指针。当然,这仍然可以被理解为“Go标准库以指针的形式实现了引用
”。不过本文本就不是为了争执“引用的正统定义”,而是为了明确几种常见语言中引用的定义,特别是Go中所谓引用与其它语言的异同之处。对各语言引用
、引用传递
的定义或原理的描述在网上很常见,不再赘述,此处用不同语言(Go/C++/C#/Java)的类似用法来直观展现它们的区别。
我们的目的是,以引用
方式传递变量到函数中,在函数中对形参进行重新实例化,看重新实例化是否会影响到外部的实参。
// Go
package main
import "fmt"
func main() {
m := make(map[int]int)
m[5] = 5
f(m)
fmt.Println("Go\t\tm[5]: ", m[5])
}
func f(m map[int]int) {
m = make(map[int]int) // 重新实例化
m[5] = 10
}
// C++
# include <iostream>
using namespace std;
struct A
{
int a;
};
int f(A& a){ // 引用方式传递
a = A(); // 重新实例化
a.a = 10;
}
int main(){
A a;
a.a = 5;
f(a);
cout << "C++\tA.a: " << a.a; // 查看 f 中为 a 声明的实例是否影响外部 a
}
// C#
using System;
namespace main
{
class A
{
public int a;
}
class main
{
static void Main(string[] args)
{
A a = new A();
a.a = 5;
f(a);
Console.WriteLine("dotnet core\tA.a: " + a.a);
}
static void f(A a){
a = new A();
a.a = 10;
}
}
}
// Java
class A{
public int a;
}
public class testJava{
public static void f(A a){
a = new A();
a.a = 10;
}
public static void main(String[] args){
A a = new A();
a.a = 5;
f(a);
System.out.println("Java\tA.a: " + a.a);
}
}
其执行结果为:
# 我对打印进行了手动对齐
Go m[5]: 5
C++ A.a: 10
dotnet core A.a: 5
Java A.a: 5
可以看到,只有在C++中,对形参的重新实例化影响到了实参。换句话说,只有C++的引用不是拷贝一份指针。
当然,我们也可以在《C++ 标准》中找到对引用的准确描述:
Here i and j are aliases for main’s x and y respectively. In other words, i is x — not a pointer to x, nor a copy of x, but x itself. Anything you do to i gets done to x, and vice versa.
i 和 j 分别是 x 和 y 的别名。换句话说,is 就是 x —— 不是 x 的指针或副本,而就是它本身。任何你对 i 做的事也同样对 x 生效,反之亦然。