• 面试官:Go函数参数传递到底是值传递还是引用传递?


    大家好,我是木川

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

    如果参数是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。

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

    先说下结论:Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。

    一、什么是值传递

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

    二、什么是引用传递?

    将实参的地址传递给形参,函数内对形参值内容的修改,将会影响实参的值内容。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\n", &i)
    6.  modifyInt(i) // args就是实际参数
    7.  fmt.Printf("改动后的值是: %v\n", i)
    8. }
    9. func modifyInt(i int64) { //这里定义的args就是形式参数
    10.  fmt.Printf("函数里接收到int的内存地址是:%p\n", &i)
    11.  i = 10
    12. }
    13. 原始int内存地址是 0xc0000180b8
    14. 函数里接收到int的内存地址是:0xc0000180c0
    15. 改动后的值是: 1

    指针类型

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

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

    slice类型

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

    1. package main
    2. import "fmt"
    3. func main() {
    4.  var s = []int64{123}
    5.  // &操作符打印出的地址是无效的,是fmt函数作了特殊处理
    6.  fmt.Printf("直接对原始切片取地址%v \n", &s)
    7.  // 打印slice的内存地址是可以直接通过%p打印的,不用使用&取地址符转换
    8.  fmt.Printf("原始切片的内存地址: %p \n", s)
    9.  fmt.Printf("原始切片第一个元素的内存地址: %p \n", &s[0])
    10.  modifySlice(s)
    11.  fmt.Printf("改动后的值是: %v\n", s)
    12. }
    13. func modifySlice(s []int64) {
    14.  // &操作符打印出的地址是无效的,是fmt函数作了特殊处理
    15.  fmt.Printf("直接对函数里接收到切片取地址%v\n", &s)
    16.  // 打印slice的内存地址是可以直接通过%p打印的,不用使用&取地址符转换
    17.  fmt.Printf("函数里接收到切片的内存地址是 %p \n", s)
    18.  fmt.Printf("函数里接收到切片第一个元素的内存地址: %p \n", &s[0])
    19.  s[0] = 10
    20. }
    21. 直接对原始切片取地址&[1 2 3
    22. 原始切片的内存地址: 0xc0000b8000 
    23. 原始切片第一个元素的内存地址: 0xc0000b8000 
    24. 直接对函数里接收到切片取地址&[1 2 3]
    25. 函数里接收到切片的内存地址是 0xc0000b8000 
    26. 函数里接收到切片第一个元素的内存地址: 0xc0000b8000 
    27. 改动后的值是: [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\n", &m)
    7.  modifyMap(m)
    8.  fmt.Printf("改动后的值是: %v\n", m)
    9. }
    10. func modifyMap(m map[string]int) {
    11.  fmt.Printf("函数里接收到map的内存地址是:%p\n", &m)
    12.  m["age"] = 9
    13. }
    14. 原始map的内存地址是:0xc00000e028
    15. 函数里接收到map的内存地址是:0xc00000e038
    16. 改动后的值是: 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\n", &p)
    9.  go func(p chan bool) {
    10.   fmt.Printf("函数里接收到chan的内存地址是:%p\n", &p)
    11.   //模拟耗时
    12.   time.Sleep(2 * time.Second)
    13.   p <- true
    14.  }(p)
    15.  select {
    16.  case l := <-p:
    17.   fmt.Printf("接收到的值是: %v\n", l)
    18.  }
    19. }
    20. 原始chan的内存地址是:0xc00000e028
    21. 函数里接收到chan的内存地址是:0xc00000e038
    22. 接收到的值是: 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\n", &per)
    13.  modifyStruct(per)
    14.  fmt.Printf("改动后的值是: %v\n", per)
    15. }
    16. func modifyStruct(per Person) {
    17.  fmt.Printf("函数里接收到struct的内存地址是:%p\n", &per)
    18.  per.Age = 10
    19. }
    20. 原始struct的内存地址是:0xc0000a6018
    21. 函数里接收到struct的内存地址是:0xc0000a6030
    22. 改动后的值是: {test 8}

    最后给自己的原创 Go 面试小册打个广告,如果你从事 Go 相关开发,欢迎扫码购买,目前 10 元买断,加下面的微信发送支付截图额外赠送一份自己录制的 Go 面试题讲解视频

    a72391c4af1752712e80070cdd2f3128.jpeg

    757d3eabcac7a24bab2c09c7c9841300.png

    如果对你有帮助,帮我点一下在看或转发,欢迎关注我的公众号

  • 相关阅读:
    selenium鼠标操作方法
    Postman传参后台接收问题
    unity简单数字拼图小游戏(源码)
    iclr 2023 投稿指南
    SpringCloud Alibaba【二】nacos
    Python生成allure测试报告,allure使用详细说明
    Android Studio Bumblebee | 2021.1.1 发布,快来看看更新了什么
    【Python】Pandas处理数据太慢,来试试Polars吧!
    SpringBoot中使用JdbcTemplate访问Oracle数据库
    Python之列表
  • 原文地址:https://blog.csdn.net/caspar_notes/article/details/133473463