扫码订阅《 》或入驻星球,即可阅读文章!

GOLANG ROADMAP

阅读模式

  • 沉浸
  • 自动
  • 日常
首页
Go学习
  • Go学院

    • Go小课
    • Go视界
    • Go小考
    • Go实战
  • Go资源

    • 优质课程
    • 在线宝典
    • 资源下载
    • 帮找资源
训练营 🔥
  • Go体系课&实战训练营
  • 升值加薪陪跑训练营
Go求职
  • 求职刷题

    • 企业题库
    • 面试宝典
    • 求职面经
  • 求职服务

    • 内推互助
    • 求职助力
    • 内推公司
Go友会
  • 城市
  • 校园
推广返佣
  • 返佣排行
  • 返佣规则
  • 推广学院
实验区
  • Go周边
  • Go宝典

    • 推荐图书
    • 精品博文
  • Go开源

    • Go仓库
    • Go月刊
更多
  • 用户中心

    • 我的信息
    • 我的返佣
    • 我的消息
  • 玩转星球

    • 星球介绍
    • 星主权益
    • 吐槽专区
    • 成长记录
  • 合作交流

    • 商务合作
    • 讲师招募
    • 生态伙伴
author-avatar

GOLANG ROADMAP


首页
Go学习
  • Go学院

    • Go小课
    • Go视界
    • Go小考
    • Go实战
  • Go资源

    • 优质课程
    • 在线宝典
    • 资源下载
    • 帮找资源
训练营 🔥
  • Go体系课&实战训练营
  • 升值加薪陪跑训练营
Go求职
  • 求职刷题

    • 企业题库
    • 面试宝典
    • 求职面经
  • 求职服务

    • 内推互助
    • 求职助力
    • 内推公司
Go友会
  • 城市
  • 校园
推广返佣
  • 返佣排行
  • 返佣规则
  • 推广学院
实验区
  • Go周边
  • Go宝典

    • 推荐图书
    • 精品博文
  • Go开源

    • Go仓库
    • Go月刊
更多
  • 用户中心

    • 我的信息
    • 我的返佣
    • 我的消息
  • 玩转星球

    • 星球介绍
    • 星主权益
    • 吐槽专区
    • 成长记录
  • 合作交流

    • 商务合作
    • 讲师招募
    • 生态伙伴
  • 课程介绍

    • 《Go语言面试题精讲》
  • Go基础

    • 1.Go 程序的基本结构?
    • 2. Go 有哪些关键字?
    • 3. Go 有哪些数据类型?
    • 4. Go 方法与函数的区别?
    • 5.Go 方法值接收者和指针接收者的区别?
    • 6. Go 函数返回局部变量的指针是否安全?
    • 7.Go 函数参数传递到底是值传递还是引用传递?
    • 8.Go defer关键字的实现原理?
    • 9.Go 内置函数make和new的区别?
  • Slice

  • Map

  • Channel

  • Mutex

  • Goroutine

  • 调度模型

  • 内存管理

  • 并发编程

扫码订阅《 》或入驻星球,即可阅读文章!

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


caspar
【点击观看视频】Go 函数参数传递到底是值传递还是引用传递?

先说下结论:

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

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

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

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

什么是值传递?

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

什么是引用传递?

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

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

int类型

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

package main

import "fmt"

func main() {
    var i int64 = 1
    fmt.Printf("原始int内存地址是 %p\n", &i)
    modifyInt(i) // args就是实际参数
    fmt.Printf("改动后的值是: %v\n", i)
}

func modifyInt(i int64) { //这里定义的args就是形式参数
    fmt.Printf("函数里接收到int的内存地址是:%p\n", &i)
    i = 10
}

原始int内存地址是 0xc0000180b8
函数里接收到int的内存地址是:0xc0000180c0
改动后的值是: 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

package main

import "fmt"

func main() {
    var args int64 = 1                  // int类型变量
    p := &args                          // 指针类型变量
    fmt.Printf("原始指针的内存地址是 %p\n", &p)   // 存放指针类型变量
    fmt.Printf("原始指针指向变量的内存地址 %p\n", p) // 存放int变量
    modifyPointer(p)                    // args就是实际参数
    fmt.Printf("改动后的值是: %v\n", *p)
}

func modifyPointer(p *int64) { //这里定义的args就是形式参数
    fmt.Printf("函数里接收到指针的内存地址是 %p \n", &p)
    fmt.Printf("函数里接收到指针指向变量的内存地址 %p\n", p)
    *p = 10
}

原始指针的内存地址是 0xc000110018
原始指针指向变量的内存地址 0xc00010c008
函数里接收到指针的内存地址是 0xc000110028 
函数里接收到指针指向变量的内存地址 0xc00010c008
改动后的值是: 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

slice类型

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

package main

import "fmt"

func main() {
    var s = []int64{1, 2, 3}
    // &操作符打印出的地址是无效的,是fmt函数作了特殊处理
    fmt.Printf("直接对原始切片取地址%v \n", &s)
    // 打印slice的内存地址是可以直接通过%p打印的,不用使用&取地址符转换
    fmt.Printf("原始切片的内存地址: %p \n", s)
    fmt.Printf("原始切片第一个元素的内存地址: %p \n", &s[0])
    modifySlice(s)
    fmt.Printf("改动后的值是: %v\n", s)
}

func modifySlice(s []int64) {
    // &操作符打印出的地址是无效的,是fmt函数作了特殊处理
    fmt.Printf("直接对函数里接收到切片取地址%v\n", &s)
    // 打印slice的内存地址是可以直接通过%p打印的,不用使用&取地址符转换
    fmt.Printf("函数里接收到切片的内存地址是 %p \n", s)
    fmt.Printf("函数里接收到切片第一个元素的内存地址: %p \n", &s[0])
    s[0] = 10
}

直接对原始切片取地址&[1 2 3] 
原始切片的内存地址: 0xc0000b8000 
原始切片第一个元素的内存地址: 0xc0000b8000 
直接对函数里接收到切片取地址&[1 2 3]
函数里接收到切片的内存地址是 0xc0000b8000 
函数里接收到切片第一个元素的内存地址: 0xc0000b8000 
改动后的值是: [10 2 3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

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

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

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

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

map类型

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

package main

import "fmt"

func main() {
    m := make(map[string]int)
    m["age"] = 8

    fmt.Printf("原始map的内存地址是:%p\n", &m)
    modifyMap(m)
    fmt.Printf("改动后的值是: %v\n", m)
}

func modifyMap(m map[string]int) {
    fmt.Printf("函数里接收到map的内存地址是:%p\n", &m)
    m["age"] = 9
}

原始map的内存地址是:0xc00000e028
函数里接收到map的内存地址是:0xc00000e038
改动后的值是: map[age:9]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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

//src/runtime/map.go
func makemap(t *maptype, hint int, h *hmap) *hmap {
    mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
    if overflow || mem > maxAlloc {
        hint = 0
    }

    // initialize Hmap
    if h == nil {
        h = new(hmap)
    }
    h.hash0 = fastrand()
}
1
2
3
4
5
6
7
8
9
10
11
12
13

channel类型

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

package main

import (
    "fmt"
    "time"
)

func main() {
    p := make(chan bool)
    fmt.Printf("原始chan的内存地址是:%p\n", &p)
    go func(p chan bool) {
        fmt.Printf("函数里接收到chan的内存地址是:%p\n", &p)
        //模拟耗时
        time.Sleep(2 * time.Second)
        p <- true
    }(p)

    select {
    case l := <-p:
        fmt.Printf("接收到的值是: %v\n", l)
    }
}

原始chan的内存地址是:0xc00000e028
函数里接收到chan的内存地址是:0xc00000e038
接收到的值是: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

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

// src/runtime/chan.go
func makechan(t *chantype, size int) *hchan {
    elem := t.elem

    // compiler checks this but be safe.
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
    if hchanSize%maxAlign != 0 || elem.align > maxAlign {
        throw("makechan: bad alignment")
    }

    mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
        panic(plainError("makechan: size out of range"))
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

struct类型

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

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    per := Person{
        Name: "test",
        Age:  8,
    }
    fmt.Printf("原始struct的内存地址是:%p\n", &per)
    modifyStruct(per)
    fmt.Printf("改动后的值是: %v\n", per)
}

func modifyStruct(per Person) {
    fmt.Printf("函数里接收到struct的内存地址是:%p\n", &per)
    per.Age = 10
}

原始struct的内存地址是:0xc0000a6018
函数里接收到struct的内存地址是:0xc0000a6030
改动后的值是: {test 8}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27