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++的引用不是拷贝一份指针。

发表评论

电子邮件地址不会被公开。 必填项已用*标注