Go语言之参数传递

文章收录在网站:http://hardyfish.top/

文章收录在网站:http://hardyfish.top/

文章收录在网站:http://hardyfish.top/

文章收录在网站:http://hardyfish.top/

在这里插入图片描述

修改参数

假设你定义了一个函数,并在函数里对参数进行修改,想让调用者可以通过参数获取你最新修改的值。

func main() {p:=person{name: "张三",age: 18}modifyPerson(p)fmt.Println("person name:",p.name,",age:",p.age)
}func modifyPerson(p person)  {p.name = "李四"p.age = 20
}type person struct {name stringage int
}

在这个示例中,我期望通过 modifyPerson 函数把参数 p 中的 name 修改为李四,把 age 修改为 20 。

代码没有错误,但是运行一下,你会看到如下打印输出:

person name: 张三 ,age: 18

怎么还是张三与 18 呢?我换成指针参数试试,如下所示:

modifyPerson(&p)
func modifyPerson(p *person)  {p.name = "李四"p.age = 20
}

这些代码用于满足指针参数的修改,把接收的参数改为指针参数,以及在调用 modifyPerson 函数时,通过&取地址符传递一个指针。

现在再运行程序,就可以看到期望的输出了,如下所示:

person name: 李四 ,age: 20

值类型

在 Go 语言中,person 是一个值类型,而 &p 获取的指针是 *person 类型的,即指针类型。

那么为什么值类型在参数传递中无法修改呢?这也要从内存讲起。

内存都有一个编号,称为内存地址。

所以要想修改内存中的数据,就要找到这个内存地址。

现在,我来对比值类型变量在函数内外的内存地址,如下所示:

func main() {p:=person{name: "张三",age: 18}fmt.Printf("main函数:p的内存地址为%p\n",&p)modifyPerson(p)fmt.Println("person name:",p.name,",age:",p.age)
}func modifyPerson(p person)  {fmt.Printf("modifyPerson函数:p的内存地址为%p\n",&p)p.name = "李四"p.age = 20
}

其中,我把原来的示例代码做了更改,分别打印出在 main 函数中变量 p 的内存地址,以及在 modifyPerson 函数中参数 p 的内存地址。

运行以上程序,可以看到如下结果:

main函数:p的内存地址为0xc0000a6020
modifyPerson函数:p的内存地址为0xc0000a6040
person name: 张三 ,age: 18

你会发现它们的内存地址都不一样,这就意味着,在 modifyPerson 函数中修改的参数 p 和 main 函数中的变量 p 不是同一个,这也是我们在 modifyPerson 函数中修改参数 p,但是在 main 函数中打印后发现并没有修改的原因。

导致这种结果的原因是 Go 语言中的函数传参都是值传递。

值传递指的是传递原来数据的一份拷贝,而不是原来的数据本身。

image-20240405204640653

以 modifyPerson 函数来说,在调用 modifyPerson 函数传递变量 p 的时候,Go 语言会拷贝一个 p 放在一个新的内存中,这样新的 p 的内存地址就和原来不一样了,但是里面的 name 和 age 是一样的,还是张三和 18。

这就是副本的意思,变量里的数据一样,但是存放的内存地址不一样。

除了 struct 外,还有浮点型、整型、字符串、布尔、数组,这些都是值类型。

指针类型

指针类型的变量保存的值就是数据对应的内存地址,所以在函数参数传递是传值的原则下,拷贝的值也是内存地址。

现在对以上示例稍做修改,修改后的代码如下:

func main() {p:=person{name: "张三",age: 18}fmt.Printf("main函数:p的内存地址为%p\n",&pmodifyPerson(&p)fmt.Println("person name:",p.name,",age:",p.age)
}func modifyPerson(p *person)  {fmt.Printf("modifyPerson函数:p的内存地址为%p\n",p)p.name = "李四"p.age = 20
}

运行这个示例,你会发现打印出的内存地址一致,并且数据也被修改成功了,如下所示:

main函数:p的内存地址为0xc0000a6020
modifyPerson函数:p的内存地址为0xc0000a6020
person name: 李四 ,age: 20

所以指针类型的参数是永远可以修改原数据的,因为在参数传递时,传递的是内存地址。

小提示:值传递的是指针,也是内存地址。通过内存地址可以找到原数据的那块内存,所以修改它也就等于修改了原数据。

引用类型

map

对于上面的例子,假如我不使用自定义的 person 结构体和指针,能不能用 map 达到修改的目的呢?

func main() {m:=make(map[string]int)m["飞雪无情"] = 18fmt.Println("飞雪无情的年龄为",m["飞雪无情"])modifyMap(m)fmt.Println("飞雪无情的年龄为",m["飞雪无情"])}func modifyMap(p map[string]int)  {p["飞雪无情"] =20}

我定义了一个 map[string]int 类型的变量 m,存储一个 Key 为飞雪无情、Value 为 18 的键值对,然后把这个变量 m 传递给函数 modifyMap。modifyMap 函数所做的事情就是把对应的值修改为 20。

现在运行这段代码,通过打印输出来看是否修改成功,结果如下所示:

飞雪无情的年龄为 18飞雪无情的年龄为 20

确实修改成功了。你是不是有不少疑惑?没有使用指针,只是用了 map 类型的参数,按照 Go 语言值传递的原则,modifyMap 函数中的 map 是一个副本,怎么会修改成功呢?

要想解答这个问题,就要从 make 这个 Go 语言内建的函数说起。在 Go 语言中,任何创建 map 的代码(不管是字面量还是 make 函数)最终调用的都是 runtime.makemap 函数。

小提示:用字面量或者 make 函数的方式创建 map,并转换成 makemap 函数的调用,这个转换是 Go 语言编译器自动帮我们做的。

从下面的代码可以看到,makemap 函数返回的是一个 *hmap 类型,也就是说返回的是一个指针,所以我们创建的 map 其实就是一个 *hmap。

// makemap implements Go map creation for make(map[k]v, hint).func makemap(t *maptype, hint int, h *hmap) *hmap{//省略无关代码}

因为 Go 语言的 map 类型本质上就是 *hmap,所以根据替换的原则,我刚刚定义的 modifyMap(p map) 函数其实就是 modifyMap(p *hmap)。这是不是和上一小节讲的指针类型的参数调用一样了?

这也是通过 map 类型的参数可以修改原始数据的原因,因为它本质上就是个指针。

为了进一步验证创建的 map 就是一个指针,我修改上述示例,打印 map 类型的变量和参数对应的内存地址,如下面的代码所示:

func main(){//省略其他没有修改的代码fmt.Printf("main函数:m的内存地址为%p\n",m)}func modifyMap(p map[string]int)  {fmt.Printf("modifyMap函数:p的内存地址为%p\n",p)//省略其他没有修改的代码}

例子中的两句打印代码是新增的,其他代码没有修改,这里就不再贴出来了。

运行修改后的程序,你可以看到如下输出:

飞雪无情的年龄为 18main函数:m的内存地址为0xc000060180modifyMap函数:p的内存地址为0xc000060180飞雪无情的年龄为 20

从输出结果可以看到,它们的内存地址一模一样,所以才可以修改原始数据,得到年龄是 20 的结果。而且我在打印指针的时候,直接使用的是变量 m 和 p,并没有用到取地址符 &,这是因为它们本来就是指针,所以就没有必要再使用 & 取地址了。

所以在这里,Go 语言通过 make 函数或字面量的包装为我们省去了指针的操作,让我们可以更容易地使用 map。其实就是语法糖,这是编程界的老传统了。

注意:这里的 map 可以理解为引用类型,但是它本质上是个指针,只是可以叫作引用类型而已。在参数传递时,它还是值传递,并不是其他编程语言中所谓的引用传递。

chan

它也可以理解为引用类型,而它本质上也是个指针。

通过下面的源代码可以看到,所创建的 chan 其实是个 *hchan,所以它在参数传递中也和 map 一样。

func makechan(t *chantype, size int64) *hchan {//省略无关代码}

严格来说,Go 语言没有引用类型,但是我们可以把 map、chan 称为引用类型,这样便于理解。

除了 map、chan 之外,Go 语言中的函数、接口、slice 切片都可以称为引用类型。

小提示:指针类型也可以理解为是一种引用类型。

类型的零值

在 Go 语言中,定义变量要么通过声明、要么通过 make 和 new 函数,不一样的是 make 和 new 函数属于显式声明并初始化。

如果我们声明的变量没有显式声明初始化,那么该变量的默认值就是对应类型的零值。

从下面的表格可以看到,可以称为引用类型的零值都是 nil。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/48345.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【已解决】Linux(Centos7)中yum过程域名无法解析问题

问题原因 Linux中yum过程域名无法解析问题,但是ping 域名时联通的(即DNS没问题),所以初步判断是镜像源的问题。 解决方法(Centos7) 1、备份/etc/yum.repos.d/CentOS-Base.repo 2、下载CentOS-Base.repo…

【Linux】Linux的基本使用

一.Linux的背景知识. 1.1什么是Linux Linux是一种开源的类Unix操作系统内核. 和Windows是" 并列 "的关系. 1.2Linux的发行版本. Linux 严格意义来说只是一个 “操作系统内核”.一个完整的操作系统 操作系统内核 配套的应用程序. 由于 Linux 是一个完全开源免费…

FPGA:二选一选择器

1、需求 使用XILINX的XC7A35TFFG484-2开发板,完成二选一选择器的设计。 2、分析 二选一选择器如下所示: 观察可知有三个输入端,一个输出端,其逻辑原理为:当sel为高电平时,outa,当sel为低电平…

十二、数组(2)

1.冒泡排序数组(升序) 冒泡排序:将一个整型数组排序(升序) 例: 10 9 8 7 6 5 4 3 2 1 9 10 8 7 6 …

第十四届蓝桥杯省赛C++C组C题【三国游戏】题解(AC)

解题思路 由于三种国家都有获胜的可能,所以我们需要分别枚举 X , Y , Z X,Y,Z X,Y,Z 获胜的情况。 设 X X X 获胜,那么对于第 i i i 个事件的贡献为 a [ i ] − ( b [ i ] c [ i ] ) a[i]-(b[i]c[i]) a[i]−(b[i]c[i]),根据贪心的策略…

【银河麒麟服务器操作系统】java进程oom现象分析及处理建议

了解银河麒麟操作系统更多全新产品,请点击访问麒麟软件产品专区:https://product.kylinos.cn 现象描述 某服务器系统升级内核至4.19.90-25.22.v2101版本后仍会触发oom导致java进程被kill。 现象分析 oom现象分析 系统messages日志分析,故…

数据编织 Data Fabric:解决“数据孤岛”的新思路

一个不争的事实是,企业内部数据孤岛的形成,根因在于业务发展的复杂性与技术迭代的快速性导致。具体而言,随着企业业务快速增长,如新生产线的引入或外部公司的并购,这些活动往往伴随着新系统上线与独立数据体系的融入&a…

【safari】react在safari浏览器中,遇到异步时间差的问题,导致状态没有及时更新到state,引起传参错误。如何解决

在safari浏览器中,可能会遇到异步时间差的问题,导致状态没有及时更新到state,引起传参错误。 PS:由于useState是一个普通的函数, 定义为() > void;因此此处不能用await/async替代setTimeout,只能用在返…

网络安全行业最大的敌人是自己

在危机四伏的数字丛林时代,网络安全行业需要跳出资本和市场的博弈陷阱,通过拯救自己来拯救所有人。当然,政府和资本也应该意识到这一点。 在当今这个数字化时代,网络安全的重要性与日俱增。然而,尽管政府和企业不断强调…

【vue+el-table】实现表尾合计行分两行显示,一行显示勾选项之和,一行显示合计,已实现,具体思路解析

效果图: 思路解析: 首先进行了el-table列表的组件封装,很多参数是传进来的。如果是普通的列表,相关参数直接定义就行 1、使用el-table的summary-method处理表尾行 (1)定义summaryIndex用于指定合计在哪一列…

vue学习笔记(十一)——开发心得(axios的封装、promise细节、vue-router开发中的使用)

1. axios的网络请求的封装 1.1 为什么要封装api? 代码分层,便于以后的修改,无需触碰逻辑页面 目标: 网络请求,不散落在各个逻辑页面里,封装起来方便以后修改 1.2 封装api步骤 ① 在项目 src 下新建目录 utlis &am…

VTD学习笔记(一)-启动vtd、基本界面和按钮

写在前面:真快啊,眨眼就毕业上班了,岗位也是做仿真,看来以后就是一直做仿真了,再见了定位~。公司使用的是vtd,看资料是一个很庞大的自动驾驶仿真软件,囊括了车辆动力学到传感器仿真,…

Python list comprehension (列表推导式 - 列表解析式 - 列表生成式)

Python list comprehension {列表推导式 - 列表解析式 - 列表生成式} 1. Python list comprehension (列表推导式 - 列表解析式 - 列表生成式)2. Example3. ExampleReferences Python 中的列表解析式并不是用来解决全新的问题,只是为解决已有问题提供新的语法。 列…

iPad型号数据解析:了解不同iPad型号的连接和扩展性能力

iPad是一款非常受欢迎的平板电脑,拥有多种型号和规格可供选择。在本篇文章中,我们将深入研究不同iPad型号的连接和扩展性能。数据源来自于挖数据平台,该平台提供了全面的iPad型号数据,共计1485个型号。 首先,让我们来…

【D3.js in Action 3 精译_020】2.6 用 D3 设置与修改元素样式 + 名人专访(Nadieh Bremer)+ 2.7 本章小结

当前内容所在位置 第一部分 D3.js 基础知识 第一章 D3.js 简介(已完结) 1.1 何为 D3.js?1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践(上)1.3 数据可视化最佳实践(下)1.4 本章小结 第二章…

怎样用Java程序与数据库建立联系?

首先我们要了解一下JDBC,一个为Java程序与关系型数据库交互提供便利的API(应用程序编程接口), 本期我们尝试用Java编程软件IDEA与MYSQL数据库建立联系。 首先我们在IDEA中穿件一个(SQL),然后导…

系统编程--Linux下文件其他操作

这里写目录标题 文件存储理论补充dentry、inode 文件其他操作stat函数作用函数原型代码(以获取文件大小为例)补充(获取文件类型) lstat函数作用函数原型代码补充(获取文件权限)总结 tipslink函数作用简介函…

畅玩游戏新选择 :游戏本 Windows10 64位 专业版!

对于喜欢游戏竞技的玩家而言,选择一款合适的操作系统对于提升游戏体验至关重要。为了满足这一需求,系统之家小编将带来高性能的游戏本专用Win10操作系统。这一版本系统不仅注重游戏的稳定性,还针对玩家在游戏中可能遇到的超时检测和恢复&…

收银系统源码-千呼新零售收银视频介绍

千呼新零售2.0系统是零售行业连锁店一体化收银系统,包括线下收银线上商城连锁店管理ERP管理商品管理供应商管理会员营销等功能为一体,线上线下数据全部打通。 适用于商超、便利店、水果、生鲜、母婴、服装、零食、百货、宠物等连锁店使用。 详细介绍请…

JavaScript 模板字符串:让字符串拼接变得更优雅

在 JavaScript 开发中,字符串拼接是一个常见的需求。从简单的用户界面文本生成到复杂的动态数据格式化,字符串操作无处不在。传统的字符串拼接方法虽然功能强大,但往往显得冗长且难以阅读。为了解决这一问题,ES6(ECMAS…