传值还是传引用

调用函数时, 传入的参数的 传值 还是 传引用, 几乎是每种编程语言都会关注的问题. 最近在使用 golang 的时候, 由于 传值传引用 的方式没有弄清楚, 导致了 BUG.

经过深入的尝试, 终于弄明白了 golang 的 传值传引用, 尝试过程记录如下, 供大家参考!

golang 本质上都是传值方式调用

严格来说, golang 中都是传值调用, 下面通过例子一一说明

普通类型的参数

这里的普通类型, 指的是 int, string 等原始的数据类型, 这些类型作为函数参数时, 都是 传值 调用. 这个基本没什么疑问.

  1. func param_ref_test01() {
  2. var t1 = 0
  3. var t2 = "000"
  4. var f1 = func(p int) {
  5. p += 1
  6. }
  7. var f2 = func(p string) {
  8. p += "-changed"
  9. }
  10. fmt.Printf(">>>调用前: t1 = %d t2 = %s\n", t1, t2)
  11. f1(t1)
  12. f2(t2)
  13. fmt.Printf("<<<调用后: t1 = %d t2 = %s\n", t1, t2)
  14. }

运行的结果:

  1. >>>调用前: t1 = 0 t2 = 000
  2. <<<调用后: t1 = 0 t2 = 000

struct 指针, map, slice 类型的参数

对于这种类型的参数, 表面上是 传引用 调用, 我也被这个表面现象迷惑过…

  1. func param_ref_test02() {
  2. type Person struct {
  3. Name string
  4. Age int
  5. }
  6. var t3 = &Person{
  7. Name: "test",
  8. Age: 10,
  9. }
  10. var t4 = []string{"a", "b", "c"}
  11. var t5 = make(map[string]int)
  12. t5["hello"] = 1
  13. t5["world"] = 2
  14. var f3 = func(p *Person) {
  15. p.Name = "test-change"
  16. p.Age = 20
  17. }
  18. var f4 = func(p []string) {
  19. p[0] = "aa"
  20. p = append(p, "d")
  21. }
  22. var f5 = func(p map[string]int) {
  23. p["hello"] = 11
  24. p["hello2"] = 22
  25. }
  26. fmt.Printf(">>>调用前: t3 = %v t4 = %v t5 = %v\n", t3, t4, t5)
  27. f3(t3)
  28. f4(t4)
  29. f5(t5)
  30. fmt.Printf("<<<调用后: t3 = %v t4 = %v t5 = %v\n", t3, t4, t5)
  31. }

运行的结果:

  1. >>>调用前: t3 = &{test 10} t4 = [a b c] t5 = map[hello:1 world:2]
  2. <<<调用后: t3 = &{test-change 20} t4 = [aa b c] t5 = map[hello:11 hello2:22 world:2]

从运行结果中, 可以看出基本符合 传引用 调用的特征, 除了 t4 的 append 没有生效之外

既然都是传值调用, 为什么 f3 内修改了 *Person, 会导致外面的 t3 改变

改造下 f3, 将变量的地址打印出来

  1. func param_ref_test03() {
  2. type Person struct {
  3. Name string
  4. Age int
  5. }
  6. var t3 = &Person{
  7. Name: "test",
  8. Age: 10,
  9. }
  10. var f3 = func(p *Person) {
  11. p.Name = "test-change"
  12. p.Age = 20
  13. fmt.Printf("参数p 指向的内存地址 = %p\n", p)
  14. fmt.Printf("参数p 内存地址 = %p\n", &p)
  15. }
  16. fmt.Printf("t3 指向的内存地址 = %p\n", t3)
  17. fmt.Printf("t3 的内存地址 = %p\n", &t3)
  18. f3(t3)
  19. }

运行的结果:

  1. t3 指向的内存地址 = 0xc00000fe20
  2. t3 的内存地址 = 0xc000010570
  3. 参数p 指向的内存地址 = 0xc00000fe20
  4. 参数p 内存地址 = 0xc000010578

从结果可以看出, t3 和 p 都是指针类型, 但是它们的内存地址是不一样的, 所以这是一个 传值 调用. 但是, 它们指向的地址(0xc00000fe20)是一样的, 所以通过 p 修改了指向的数据(*Person), t3 指向的数据也发生了变化.

只要 p 的指向地址变化, 就不会影响 t3 的变化了

  1. var f3 = func(p *Person) {
  2. p = &Person{} // 这行会改变p指向的地址
  3. p.Name = "test-change"
  4. p.Age = 20
  5. }
  6. f3(t3)

可以试试看, 只要加上上面代码中有注释的那行, 调用 f3 就不会改变 t3 了.

既然都是传值调用, 为什么 f4 内修改了 []string, 会导致外面的 t4 改变

golang 中的 slice 也是指针类型, 所以和上面 *Person 的原因一样

为什么 f4 内对 []string append 之后, 没有导致外面的 t4 改变

代码是最好的解释, 先观察 append 之后内存地址的变化, 我们再分析

  1. func param_ref_test04() {
  2. var s = []string{"a", "b", "c"}
  3. fmt.Printf("s 的内存地址 = %p\n", &s)
  4. fmt.Printf("s 指向的内存地址 = %p\n", s)
  5. s[0] = "aa"
  6. fmt.Printf("修改s[0] 之后, s 的内存地址 = %p\n", &s)
  7. fmt.Printf("修改s[0] 之后, s 指向的内存地址 = %p\n", s)
  8. s = append(s, "d")
  9. fmt.Printf("append之后, s 的内存地址 = %p\n", &s)
  10. fmt.Printf("append之后, s 指向的内存地址 = %p\n", s)
  11. }

运行的结果:

  1. s 的内存地址 = 0xc00008fec0
  2. s 指向的内存地址 = 0xc00016d530
  3. 修改s[0] 之后, s 的内存地址 = 0xc00008fec0
  4. 修改s[0] 之后, s 指向的内存地址 = 0xc00016d530
  5. append之后, s 的内存地址 = 0xc00008fec0
  6. append之后, s 指向的内存地址 = 0xc000096f00

首先, 无论是修改 slice 中的元素, 还是添加 slice 的元素, 都不会改变 s 本身的地址(0xc00008fec0) 其次, 修改 slice 中的元素, 不会改变 s 指向的地址(0xc00016d530), 所有在 f4 中修改 slice 的元素, 也会改变函数 f4 外面的变量 最后, append 操作会修改 s 指向的地址, append 之后, s 和 函数 f4 外的变量已经不是指向同一地址了, 所以 append 的元素不会影响函数 f4 外的变量

既然都是传值调用, 为什么 f5 内修改了 map, 会导致外面的 t5 改变

map 类型也是指针类型, 所以原因和上面的 *Person 一样

为什么 f5 内增加了 map 中元素, 会导致外面的 t5 改变, 没有像 t4 那样, 只变修改的部分, 不变新增的部分

同样, 看代码

  1. func param_ref_test05() {
  2. var m = make(map[string]int)
  3. m["hello"] = 1
  4. m["world"] = 2
  5. fmt.Printf("m 的内存地址 = %p\n", &m)
  6. fmt.Printf("m 指向的内存地址 = %p\n", m)
  7. m["hello"] = 11
  8. fmt.Printf("修改m 之后, m 的内存地址 = %p\n", &m)
  9. fmt.Printf("修改m 之后, m 指向的内存地址 = %p\n", m)
  10. m["hello2"] = 22
  11. fmt.Printf("追加元素之后, m 的内存地址 = %p\n", &m)
  12. fmt.Printf("追加元素之后, m 指向的内存地址 = %p\n", m)
  13. }

运行的结果:

  1. m 的内存地址 = 0xc000010598
  2. m 指向的内存地址 = 0xc000151590
  3. 修改m 之后, m 的内存地址 = 0xc000010598
  4. 修改m 之后, m 指向的内存地址 = 0xc000151590
  5. 追加元素之后, m 的内存地址 = 0xc000010598
  6. 追加元素之后, m 指向的内存地址 = 0xc000151590

根据上面的分析经验, 一目了然, 因为无论是修改还是添加 map 中的元素, m 指向的地址(0xc000151590)都没变, 所以函数 f5 中 map 参数修改元素, 添加元素之后, 都会影响函数 f5 之外的变量.

注意 这里并不是说 map 类型的参数就是 传引用 调用, 它仍然是 传值 调用, 参数 map 的地址和函数 f5 外的变量 t5 的地址是不一样的 如果在函数 f5 中修改的 map 类型参数的指向地址, 就会像传值调用那样, 不影响函数 f5 外 t5 的值

  1. func param_ref_test06() {
  2. var t5 = make(map[string]int)
  3. t5["hello"] = 1
  4. t5["world"] = 2
  5. var f5 = func(p map[string]int) {
  6. fmt.Printf("修改前 参数p 指向的内存地址 = %p\n", p)
  7. fmt.Printf("修改前 参数p 内存地址 = %p\n", &p)
  8. p = make(map[string]int) // 这行改变了 p 的指向, 使得 p 和 t5 不再指向同一个地方
  9. p["hello"] = 11
  10. p["hello2"] = 22
  11. fmt.Printf("修改后 参数p 指向的内存地址 = %p\n", p)
  12. fmt.Printf("修改后 参数p 内存地址 = %p\n", &p)
  13. }
  14. fmt.Printf("t5 指向的内存地址 = %p\n", t5)
  15. fmt.Printf("t5内存地址 = %p\n", &t5)
  16. fmt.Printf(">>>调用前: t5 = %v\n", t5)
  17. f5(t5)
  18. fmt.Printf("<<<调用后: t5 = %v\n", t5)
  19. }

运行的结果:

  1. t5 指向的内存地址 = 0xc000151590
  2. t5内存地址 = 0xc000010598
  3. >>>调用前: t5 = map[hello:1 world:2]
  4. 修改前 参数p 指向的内存地址 = 0xc000151590
  5. 修改前 参数p 内存地址 = 0xc0000105a0
  6. 修改后 参数p 指向的内存地址 = 0xc000151650
  7. 修改后 参数p 内存地址 = 0xc0000105a0
  8. <<<调用后: t5 = map[hello:1 world:2]

虽然是 map 类型参数, 但是调用前后, t5 的值没有改变.

总结

上面的尝试不敢说有多全, 但基本可以弄清 golang 函数传参的本质.

  1. 对于普通类型(int, string 等等), 就是 传值 调用, 函数内对参数的修改, 不影响外面的变量
  2. 对于 struct 指针, slice 和 map 类型, 函数内对参数的修改之所以能影响外面, 是因为参数和外面的变量指向了同一块数据的地址
  3. 对于 struct 指针, slice 和 map 类型, 函数的参数和外面的变量的地址是不一样的, 所以本质上还是 传值 调用
  4. slice 的 append 操作会改变 slice 指针的地址, 这个非常重要!!! 我曾经写了一个基于 slice 的排序算法在这个上面吃了大亏, 调研很久才发现原因…

golang的传值调用和传引用调用的更多相关文章

  1. 深刻理解C#的传值调用和传引用调用

    传值调用和传引用调用是几乎所有主流语言都会涉及到的问题,下面我谈谈我对C#中传值调用和传引用调用的理解. 1. 一般对C#中传值调用和传引用调用的理解 如果传递的参数是基元类型(int,float等) ...

  2. Java中的形参和实参的区别以及传值调用和传引用调用

    名词解析: 1.形参:用来接收调用该方法时传递的参数.只有在被调用的时候才分配内存空间,一旦调用结束,就释放内存空间.因此仅仅在方法内有效. 2.实参:传递给被调用方法的值,预先创建并赋予确定值. 3 ...

  3. 【Java-Method】读《重构》有感_Java方法到底是传值调用还是传引用调用(传钥匙调用)

    今天读<重构>P279, Separate Query from Modifier,将查询函数和修改函数分离. 问题的产生 突然想到 Java 的传对象作为参数的方法到底是 传引用调用,还 ...

  4. Python FAQ1:传值,还是传引用?

    在C/C++中,传值和传引用是函数参数传递的两种方式.由于思维定式,从C/C++转过来的Python初学者也经常会感到疑惑:在Python中,函数参数传递是传值,还是传引用呢? 看下面两段代码: de ...

  5. Python中参数是传值,还是传引用?

    在 C/C++ 中,传值和传引用是函数参数传递的两种方式,在Python中参数是如何传递的?回答这个问题前,不如先来看两段代码. 代码段1: def foo(arg): arg = 2 print(a ...

  6. Python 函数中,参数是传值,还是传引用?

    在 C/C++ 中,传值和传引用是函数参数传递的两种方式,在Python中参数是如何传递的?回答这个问题前,不如先来看两段代码. 代码段1: def foo(arg): arg = 2 print(a ...

  7. 拷贝构造函数不能传值,只能传引用,而且一般是传const引用

    为什么呢?因为传值函数,需要调用拷贝构造函数,那就层层循环无止境了.

  8. php中传值与传引用的区别。什么时候传值什么时候传引用?

    值传递:   函数范围内对值的任何改变在函数外部都会被忽略; 引用传递: 函数范围内对值的任何改变在函数外部也能反映出这些修改: 优缺点:按值传递时,php必须复制值.特别是对于大型的字符串和对象来说 ...

  9. Java内存管理-Stackoverflow问答-Java是传值还是传引用?(十一)

    勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 本文导图: 一.由一个提问引发的思考 在Stack Overflow 看到这样一个问题 ...

随机推荐

  1. Cisco TrustSec(理解)

    1.Cisco TrustSec的限制当指定了无效的设备ID时,受保护的访问凭据(Protected access credential,PAC)设置将失败并保持挂起状态. 即使在清除PAC并配置正确 ...

  2. Robot Framework 使用【2】-- MAC系统搭建Robot Framework

    前言 上一篇中讲述了如何在windows环境下搭建Robot Framework,发完帖后有几位小伙伴就私下留言有没有MAC版本的搭建过程,由于笔者MAC上是安装了旧版本的,经过笔者本周零碎时间的尝试 ...

  3. XCOJ 1205 A.First Blood

    1205: A.First Blood 时间限制: 1 Sec  内存限制: 64 MB提交: 152  解决: 44 标签提交统计讨论版 题目描述 盖伦是个小学一年级的学生,在一次数学课的时候,老师 ...

  4. jenkins windows 2.204版,免安装,推荐插件齐备.

    windows专用,已安装好推荐插件, 更新了安装源为清华源,也就是说只要官方的插件,你都可以秒速下载了.香不? 解压到一个文件夹,管理员模式运行cmdcd 文件夹名jenkins install这样 ...

  5. 20199317 myod实验

    myod实验 实验内容: 1 复习c文件处理内容 2 编写myod.c 用myod XXX实现Linux下od -tx -tc XXX的功能 3 main与其他分开,制作静态库和动态库 4 编写Mak ...

  6. linux下的npm安装

    curl --silent --location https://rpm.nodesource.com/setup_10.x | bash - yum install -y nodejs npm in ...

  7. 转载--centos7.4安装docker

    参考博文:https://www.cnblogs.com/yufeng218/p/8370670.html 作者:风止雨歇 Docker从1.13版本之后采用时间线的方式作为版本号,分为社区版CE和企 ...

  8. MySQL8.0 ROW_NUMBER、RANK、DENSE_RANK窗口函数 分组排序排名

    MySQL8.0 (ROW_NUMBER)窗口函数 排名 暂时理解函数意义,后面再进行优化,如果有关变量排序,查看这个大哥的 mysql的分组排序和变量赋值顺序 先查看一个例子: # 按照每科课程分数 ...

  9. Navicat Premium 12安装及激活

    一.安装 百度云下载地址:https://pan.baidu.com/s/1T5BjpBqLtwCy26szcKSdKw 提取码:ujzx 二.激活步骤 ①将navicat-keygen-for-x6 ...

  10. hadoop集群的各部分一般都会使用到多个端口,有些是daemon之间进行交互之用,有些是用于RPC访问以及HTTP访问。而随着hadoop周边组件的增多,完全记不住哪个端口对应哪个应用,特收集记录如此,以便查询。这里包含我们使用到的组件:HDFS, YARN, Hbase, Hive, ZooKeeper:

    组件 节点 默认端口 配置 用途说明 HDFS DataNode 50010 dfs.datanode.address datanode服务端口,用于数据传输 HDFS DataNode 50075 ...