在Go中将引用和值传递给函数

32
我对在Go中的引用传递和值传递有些困惑。
我看过这个解释中关于类型前面的*号。
*号在类型名称前面表示,声明的变量将存储另一个该类型变量的地址(而不是该类型的值)。
这对我来说完全没有意义。
在Java中,如果我将一个数据库实例传递到一个函数中,我会这样做:
 databaseFunction(DatabaseType db) {
      // do something
}

然而,在我提供的示例中,它是这样传递的。
func PutTasks(db *sql.DB) echo.HandlerFunc {

}

为什么我们需要在类型前面加上星号呢?
根据我找到的这份备忘单。
``` func PrintPerson(p *Person) 只接收指针地址(引用) ```
我不明白为什么我只想要将指针地址作为参数传递。
3个回答

72
首先,Go语言在技术上只有传值。当传递一个对象的指针时,实际上是通过值传递指针,而不是通过引用传递对象。这种差异微妙但偶尔会有影响。例如,你可以覆盖指针的值,而对调用者没有影响,与解引用并覆盖其所指向的内存不同。
// *int means you *must* pass a *int (pointer to int), NOT just an int!
func someFunc(x *int) {
    *x = 2 // Whatever variable caller passed in will now be 2
    y := 7
    x = &y // has no impact on the caller because we overwrote the pointer value!
}

关于你的问题“为什么我们需要在类型前面加上星号?”:星号表示该值是指向`sql.DB`类型的指针,而不是`sql.DB`类型的值。这两者是不能互换的!
为什么要发送指针地址呢?这样你就可以在函数的调用者和函数体之间共享值,并且在函数内部进行的更改会反映在调用者身上(例如,指针是“setter”方法在对象上工作的唯一方式)。实际上,你的Java代码也是这样做的;在Java中,你总是通过引用(指针)访问对象,所以Java会自动处理这个过程,而不需要你明确指示。但在Go语言中,你也可以通过非指针访问对象,所以你必须明确指出。如果你调用一个函数并直接传入一个对象,那么函数将得到该对象的副本,如果函数修改了该对象,调用者将看不到这些变化。因此,如果你希望变化在函数外部传播,你必须传递一个指针。这样,指针将被复制,但它所指向的对象将被共享。
参见:指针的Go教程部分指针的Go规范部分地址运算符的Go规范部分

12
指针被传递的另一个原因是为了减少被传递的值的大小。指针的大小为单个机器字(根据系统结构,可能为4或8个字节)。 sql.DB是一个结构,大约有180个字节的大小。复制这么多数据到函数调用中是很多的。 - Kaedys
5
180字节并不算太多,但在某些情况下这是使用指针的有效理由。通常情况下,我不会使用这种推理,除非性能分析表明它解决了实际问题。过早优化是万恶之源,等等。 - Adrian
5
180字节相当微不足道。单个高速缓存线基本上是免费的,大多数常见CPU在单个传输中会传输多达8行数据。仅解引用一个指针可能需要更长时间,或者不需要,这取决于数据局部性和高速缓存状态。我通过删除指针来优化了许多代码片段。首先要为其功能使用指针。 - JimB
10
@Adrian Java始终按值传递。 - krulik
6
@krulik这是一种看待问题的方式,但对于大多数开发人员来说并不实用。由于Java中的所有对象都是引用,因此每当您传递一个对象时,您都是传递一个引用。因此,按引用传递的语义适用于绝大部分的Java代码。 - Adrian
显示剩余9条评论

28

引用语义的目的是允许函数操纵其范围之外的数据。比较:

func BrokenSwap(a int, b int) {
  a, b = b, a
}

func RealSwap(a *int, b *int) {
  *a, *b = *b, *a
}
当你调用BrokenSwap(x, y)时,没有任何效果,因为该函数接收和操作的是数据的私有副本。相比之下,当你调用RealSwap(&x, &y)时,实际上交换了调用者xy的值。在调用处显式地取变量的地址告诉读者这些变量可能被更改。

1

按引用传递:

当你通过不同的名称将相同变量传递到函数中时。

以下是来自C ++(因为Go没有这个概念)的示例,其中a和a1是同一变量。

void swap(int& a1, int& b1)
{
    int tmp = a1;
    a1 = b1;
    b1 = tmp;
}
 
int main()
{
    int a = 10, b = 20;
    swap(a, b);
    cout << "a " << a << " b " << b ;
}

Go将所有内容都作为数据传递(这意味着它将当前活动帧中的数据复制到新函数的新活动帧中)。因此,如果您传递值,它会复制该值,并且优点是避免了意外修改。当它传递变量的地址时,它也被复制到新指针变量中,但具有更高的效率优势,因为指针的大小更小。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接