Go函数参数传递到底是值传递还是引用传递?

先说下结论:

Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。

参数如果是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;如果是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。

是否可以修改原内容数据,和传值、传引用没有必然的关系。在C++中,传引用肯定是可以修改原内容数据的,在Go语言里,虽然只有传值,但是我们也可以修改原内容数据,因为参数是引用类型

引用类型和引用传递是2个概念,切记!!!

什么是值传递?

将实参的值传递给形参,形参是实参的一份拷贝,实参和形参的内存地址不同。函数内对形参值内容的修改,是否会影响实参的值内容,取决于参数是否是引用类型

什么是引用传递?

将实参的地址传递给形参,函数内对形参值内容的修改,将会影响实参的值内容。Go语言是没有引用传递的,在C++中,函数参数的传递方式有引用传递。

下面分别针对Go的值类型(int、struct等)、引用类型(指针、slice、map、channel),验证是否是值传递,以及函数内对形参的修改是否会修改原内容数据

int类型

形参和实际参数内存地址不一样,证明是指传递;参数是值类型,所以函数内对形参的修改,不会修改原内容数据

  1. package main
  2. import "fmt"
  3. func main() {
  4. var i int64 = 1
  5. fmt.Printf("原始int内存地址是 %p
  6. ", &i)
  7. modifyInt(i) // args就是实际参数
  8. fmt.Printf("改动后的值是: %v
  9. ", i)
  10. }
  11. func modifyInt(i int64) { //这里定义的args就是形式参数
  12. fmt.Printf("函数里接收到int的内存地址是:%p
  13. ", &i)
  14. i = 10
  15. }
  16. 原始int内存地址是 0xc0000180b8
  17. 函数里接收到int的内存地址是:0xc0000180c0
  18. 改动后的值是: 1

指针类型

形参和实际参数内存地址不一样,证明是指传递,由于形参和实参是指针,指向同一个变量。函数内对指针指向变量的修改,会修改原内容数据

  1. package main
  2. import "fmt"
  3. func main() {
  4. var args int64 = 1 // int类型变量
  5. p := &args // 指针类型变量
  6. fmt.Printf("原始指针的内存地址是 %p
  7. ", &p) // 存放指针类型变量
  8. fmt.Printf("原始指针指向变量的内存地址 %p
  9. ", p) // 存放int变量
  10. modifyPointer(p) // args就是实际参数
  11. fmt.Printf("改动后的值是: %v
  12. ", *p)
  13. }
  14. func modifyPointer(p *int64) { //这里定义的args就是形式参数
  15. fmt.Printf("函数里接收到指针的内存地址是 %p
  16. ", &p)
  17. fmt.Printf("函数里接收到指针指向变量的内存地址 %p
  18. ", p)
  19. *p = 10
  20. }
  21. 原始指针的内存地址是 0xc000110018
  22. 原始指针指向变量的内存地址 0xc00010c008
  23. 函数里接收到指针的内存地址是 0xc000110028
  24. 函数里接收到指针指向变量的内存地址 0xc00010c008
  25. 改动后的值是: 10

slice类型

形参和实际参数内存地址一样,不代表是引用类型;下面进行详细说明slice还是值传递,传递的是指针

  1. package main
  2. import "fmt"
  3. func main() {
  4. var s = []int64{1, 2, 3}
  5. // &操作符打印出的地址是无效的,是fmt函数作了特殊处理
  6. fmt.Printf("直接对原始切片取地址%v
  7. ", &s)
  8. // 打印slice的内存地址是可以直接通过%p打印的,不用使用&取地址符转换
  9. fmt.Printf("原始切片的内存地址: %p
  10. ", s)
  11. fmt.Printf("原始切片第一个元素的内存地址: %p
  12. ", &s[0])
  13. modifySlice(s)
  14. fmt.Printf("改动后的值是: %v
  15. ", s)
  16. }
  17. func modifySlice(s []int64) {
  18. // &操作符打印出的地址是无效的,是fmt函数作了特殊处理
  19. fmt.Printf("直接对函数里接收到切片取地址%v
  20. ", &s)
  21. // 打印slice的内存地址是可以直接通过%p打印的,不用使用&取地址符转换
  22. fmt.Printf("函数里接收到切片的内存地址是 %p
  23. ", s)
  24. fmt.Printf("函数里接收到切片第一个元素的内存地址: %p
  25. ", &s[0])
  26. s[0] = 10
  27. }
  28. 直接对原始切片取地址&[1 2 3]
  29. 原始切片的内存地址: 0xc0000b8000
  30. 原始切片第一个元素的内存地址: 0xc0000b8000
  31. 直接对函数里接收到切片取地址&[1 2 3]
  32. 函数里接收到切片的内存地址是 0xc0000b8000
  33. 函数里接收到切片第一个元素的内存地址: 0xc0000b8000
  34. 改动后的值是: [10 2 3]

slice是一个结构体,他的第一个元素是一个指针类型,这个指针指向的是底层数组的第一个元素。当参数是slice类型的时候,fmt.printf通过%p打印的slice变量的地址其实就是内部存储数组元素的地址,所以打印出来形参和实参内存地址一样。

  1. type slice struct {
  2. array unsafe.Pointer // 指针
  3. len int
  4. cap int
  5. }

因为slice作为参数时本质是传递的指针,上面证明了指针也是值传递,所以参数为slice也是值传递,指针指向的是同一个变量,函数内对形参的修改,会修改原内容数据

单纯的从slice这个结构体看,我们可以通过modify修改存储元素的内容,但是永远修改不了len和cap,因为他们只是一个拷贝,如果要修改,那就要传递&slice作为参数才可以。

map类型

形参和实际参数内存地址不一样,证明是值传递

  1. package main
  2. import "fmt"
  3. func main() {
  4. m := make(map[string]int)
  5. m["age"] = 8
  6. fmt.Printf("原始map的内存地址是:%p
  7. ", &m)
  8. modifyMap(m)
  9. fmt.Printf("改动后的值是: %v
  10. ", m)
  11. }
  12. func modifyMap(m map[string]int) {
  13. fmt.Printf("函数里接收到map的内存地址是:%p
  14. ", &m)
  15. m["age"] = 9
  16. }
  17. 原始map的内存地址是:0xc00000e028
  18. 函数里接收到map的内存地址是:0xc00000e038
  19. 改动后的值是: map[age:9]

通过make函数创建的map变量本质是一个hmap类型的指针*hmap,所以函数内对形参的修改,会修改原内容数据

  1. //src/runtime/map.go
  2. func makemap(t *maptype, hint int, h *hmap) *hmap {
  3. mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
  4. if overflow || mem > maxAlloc {
  5. hint = 0
  6. }
  7. // initialize Hmap
  8. if h == nil {
  9. h = new(hmap)
  10. }
  11. h.hash0 = fastrand()
  12. }

channel类型

形参和实际参数内存地址不一样,证明是值传递

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. p := make(chan bool)
  8. fmt.Printf("原始chan的内存地址是:%p
  9. ", &p)
  10. go func(p chan bool) {
  11. fmt.Printf("函数里接收到chan的内存地址是:%p
  12. ", &p)
  13. //模拟耗时
  14. time.Sleep(2 * time.Second)
  15. p <- true
  16. }(p)
  17. select {
  18. case l := <-p:
  19. fmt.Printf("接收到的值是: %v
  20. ", l)
  21. }
  22. }
  23. 原始chan的内存地址是:0xc00000e028
  24. 函数里接收到chan的内存地址是:0xc00000e038
  25. 接收到的值是: true

通过make函数创建的chan变量本质是一个hchan类型的指针*hchan,所以函数内对形参的修改,会修改原内容数据

  1. // src/runtime/chan.go
  2. func makechan(t *chantype, size int) *hchan {
  3. elem := t.elem
  4. // compiler checks this but be safe.
  5. if elem.size >= 1<<16 {
  6. throw("makechan: invalid channel element type")
  7. }
  8. if hchanSize%maxAlign != 0 || elem.align > maxAlign {
  9. throw("makechan: bad alignment")
  10. }
  11. mem, overflow := math.MulUintptr(elem.size, uintptr(size))
  12. if overflow || mem > maxAlloc-hchanSize || size < 0 {
  13. panic(plainError("makechan: size out of range"))
  14. }
  15. }

struct类型

形参和实际参数内存地址不一样,证明是值传递。形参不是引用类型或者指针类型,所以函数内对形参的修改,不会修改原内容数据

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. Name string
  5. Age int
  6. }
  7. func main() {
  8. per := Person{
  9. Name: "test",
  10. Age: 8,
  11. }
  12. fmt.Printf("原始struct的内存地址是:%p
  13. ", &per)
  14. modifyStruct(per)
  15. fmt.Printf("改动后的值是: %v
  16. ", per)
  17. }
  18. func modifyStruct(per Person) {
  19. fmt.Printf("函数里接收到struct的内存地址是:%p
  20. ", &per)
  21. per.Age = 10
  22. }
  23. 原始struct的内存地址是:0xc0000a6018
  24. 函数里接收到struct的内存地址是:0xc0000a6030
  25. 改动后的值是: {test 8}