Golang:值传递还是引用传递

Published: 2022-09-11

Tags: Golang

本文总阅读量

问题

Golang 中向函数传参,是值传递还是引用传递呢? 先抛结论:值传递,拷贝的是值的副本。

试验

有时看起来会“不符合预期”,以下通过代码进行说明,进而了解 Go 类型的值类型和引用类型。

package main

import (
  "fmt"
)

func main() {
    s := []int{1, 2, 3, 4, 5}

    fmt.Printf("%p %v\n", s, s)
    modify(s)
    fmt.Printf("%p %v\n", s, s)
}

func modify(s2[]int) {

    fmt.Printf("%p %v\n", s2, s2)
    s2 = s2[1:3]
    s2[0] = 100
    fmt.Printf("%p %v\n", s2, s2)

}

输出

0xc000122060 [1 2 3 4 5]
0xc000122060 [1 2 3 4 5]
0xc000122068 [100 3]
0xc000122060 [1 100 3 4 5]

以传值解释看似“解释不通”,第二个 Printf 的打印的 s2 应该是不同的地址才对,但现在它跟 s 的地址是相同的,更 “离谱” 的是第四个打印的 s 值都被修改了,怎么看都像是引用,怎么说 Go 中只有值传递呢

这是因为 Golang 中数据类型可以分为两种,一种是值类型(int、float、bool、struct 等),另一种是引用类型(slice、map、channel、接口等),值类型的变量存储的是值,而引用类型的变量内存储着一个内存地址,这个内存地址存储着数据。

Slice 类型为例,指针地址指向着这个 slice 的第一个元素。

当向函数内传参时触发拷贝,值类型创建新变量,拷贝了数据,引用类型也创建了新变量,拷贝的是包含指向数据指针的数据,指针指向的是同一个内存地址,这也就是为什么修改 s2 也会影响到 s 的原因。

另外,第三行输出的地址是 slice 中第三个元素的地址。

源代码中 slice 是一个结构体,slice 变量不仅存储指针,也存储了描述数据长度了变量容量的 len 和 cap

// https://github.com/golang/go/blob/master/src/runtime/slice.go
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

再写一个 int 类型的传值看看结果

func main() {
    a := 1

    fmt.Printf("%p %v\n", &a, a)
    modify(a)
    fmt.Printf("%p %v\n", &a, a)
}

func modify(a2 int) {

    fmt.Printf("%p %v\n", &a2, a2)
    a2 = 100
    fmt.Printf("%p %v\n", &a2, a2)
}

结果

0xc0000b2008 1
0xc0000b2020 1
0xc0000b2020 100
0xc0000b2008 1

因为值类型的变量直接存储数据,相当于拷贝了数据,存在不同的内存地址,所以它们的地址不一样,对函数内变量值的修改自然也不会影响到函数外部的变量。

在回到 Slice 示例,上方 slice 结构体我们知道 slice 类型不仅保存着指向数据的指针,还保存着 len 和 cap,当发生拷贝时,三个值都进行了复制,len 和 cap 因为是 int 类型的数据,结合 int 类型传值示例,如果在函数内部 len 和 cap 发生了改变,有理由 “猜测” 不会影响到函数外部变量的 len 和 cap。

func main() {

    s := make([]int, 3, 4)
    s[0] = 1
    s[1] = 2
    s[2] = 3

    fmt.Printf("%p %v %d %d\n", s, s, len(s), cap(s))
    // fmt.Printf("%p %v %d %d\n", s, s[:4], len(s), cap(s))
    modify(s)
    fmt.Printf("%p %v %d %d\n", s, s, len(s), cap(s))
    // fmt.Printf("%p %v %d %d\n", s, s[:4], len(s), cap(s))
}

func modify(s2[]int) {

    fmt.Printf("%p %v %d %d\n", s2, s2, len(s2), cap(s2))
    s2 = append(s2, 4)
    fmt.Printf("%p %v %d %d\n", s2, s2, len(s2), cap(s2))

}

结果

0xc000018100 [1 2 3] 3 4
0xc000018100 [1 2 3] 3 4
0xc000018100 [1 2 3 4] 4 4
0xc000018100 [1 2 3] 3 4

通过示例可以看到地址没有变化,而且正如猜测,append 操作后,s2 的 len 值改变但外部的 len 没有变,结合以上的示例,还有更有意思的地方,取消两行 Printf 代码注释,使用 s[:4] 强制输出 slice 第四个元素,结果如下

0xc000136000 [1 2 3] 3 4
0xc000136000 [1 2 3 0] 3 4 -- 新增
0xc000136000 [1 2 3] 3 4
0xc000136000 [1 2 3 4] 4 4
0xc000136000 [1 2 3] 3 4
0xc000136000 [1 2 3 4] 3 4 -- 新增

可以看到在申请 slice 时候指定 cap: 4,第四个元素默认为 0,在函数中通过 append 后,第四个位置被赋值为 4,在函数外原始 s 的 len 值没有变,但 s 的第四位数据变成了 4,因为 len 的限制,如果不强制输出,没有打印出来。 需要注意的是,当 cap 不够的时候,Golang 会按照 slice 扩容规则创建一个新的内存地址,将数据复制过去。

s2 = append(s2, 4, 5) // 添加一个元素 5

做如上改动后,再运行程序,输出如下:

0xc00012c000 [1 2 3] 3 4
0xc00012c000 [1 2 3 0] 3 4
0xc00012c000 [1 2 3] 3 4
0xc000130000 [1 2 3 4 5] 5 8
0xc00012c000 [1 2 3] 3 4
0xc00012c000 [1 2 3 0] 3 4

可见在函数中 append 赋值操作后 s2 拥有了自己的数据地址,对其修改不会影响函数外的数据。

那么最后,只需要记住一个结论:

结论

在 Golang 中只有值传递(要么是该值的副本,要么是指针的副本),不存在引用传递。

参考

  1. Go 的参数是传值还是传引用问题
  2. slice是完美的引用传递吗
  3. 如何利用unsafe获取slice&map的长度