Go语言参数传递是传值还是传引用
作者:飞雪无情的博客 发布时间:2024-02-20 02:32:34
目录
什么是传值(值传递)
什么是传引用(引用传递)
迷惑Map
chan类型
和map、chan都不一样的slice
小结
对于了解一门语言来说,会关心我们在函数调用的时候,参数到底是传的值,还是引用?
其实对于传值和传引用,是一个比较古老的话题,做研发的都有这个概念,但是可能不是非常清楚。对于我们做Go语言开发的来说,也想知道到底是什么传递。
那么我们先来看看什么是值传递,什么是引用传递。
什么是传值(值传递)
传值的意思是:函数传递的总是原来这个东西的一个副本,一副拷贝。比如我们传递一个int类型的参数,传递的其实是这个参数的一个副本;传递一个指针类型的参数,其实传递的是这个该指针的一份拷贝,而不是这个指针指向的值。
对于int这类基础类型我们可以很好的理解,它们就是一个拷贝,但是指针呢?我们觉得可以通过它修改原来的值,怎么会是一个拷贝呢?下面我们看个例子。
func main() {
i:=10
ip:=&i
fmt.Printf("原始指针的内存地址是:%p\n",&ip)
modify(ip)
fmt.Println("int值被修改了,新值为:",i)
}
func modify(ip *int){
fmt.Printf("函数里接收到的指针的内存地址是:%p\n",&ip)
*ip=1
}
我们运行,可以看到输入结果如下:
原始指针的内存地址是:0xc42000c028
函数里接收到的指针的内存地址是:0xc42000c038
int值被修改了,新值为: 1
首先我们要知道,任何存放在内存里的东西都有自己的地址,指针也不例外,它虽然指向别的数据,但是也有存放该指针的内存。
所以通过输出我们可以看到,这是一个指针的拷贝,因为存放这两个指针的内存地址是不同的,虽然指针的值相同,但是是两个不同的指针。
通过上面的图,可以更好的理解。 首先我们看到,我们声明了一个变量i,值为10,它的内存存放地址是0xc420018070,通过这个内存地址,我们可以找到变量i,这个内存地址也就是变量i的指针ip。
指针ip也是一个指针类型的变量,它也需要内存存放它,它的内存地址是多少呢?是0xc42000c028。 在我们传递指针变量ip给modify函数的时候,是该指针变量的拷贝,所以新拷贝的指针变量ip,它的内存地址已经变了,是新的0xc42000c038。
不管是0xc42000c028还是0xc42000c038,我们都可以称之为指针的指针,他们指向同一个指针0xc420018070,这个0xc420018070又指向变量i,这也就是为什么我们可以修改变量i的值。
什么是传引用(引用传递)
Go语言(Golang)是没有引用传递的,这里我不能使用Go举例子,但是可以通过说明描述。
以上面的例子为例,如果在modify函数里打印出来的内存地址是不变的,也是0xc42000c028,那么就是引用传递。
迷惑Map
了解清楚了传值和传引用,但是对于Map类型来说,可能觉得还是迷惑,一来我们可以通过方法修改它的内容,二来它没有明显的指针。
func main() {
persons:=make(map[string]int)
persons["张三"]=19
mp:=&persons
fmt.Printf("原始map的内存地址是:%p\n",mp)
modify(persons)
fmt.Println("map值被修改了,新值为:",persons)
}
func modify(p map[string]int){
fmt.Printf("函数里接收到map的内存地址是:%p\n",&p)
p["张三"]=20
}
运行打印输出:
原始map的内存地址是:0xc42000c028
函数里接收到map的内存地址是:0xc42000c038
map值被修改了,新值为: map[张三:20]
两个内存地址是不一样的,所以这又是一个值传递(值的拷贝),那么为什么我们可以修改Map的内容呢?先不急,我们先看一个自己实现的struct。
func main() {
p:=Person{"张三"}
fmt.Printf("原始Person的内存地址是:%p\n",&p)
modify(p)
fmt.Println(p)
}
type Person struct {
Name string
}
func modify(p Person) {
fmt.Printf("函数里接收到Person的内存地址是:%p\n",&p)
p.Name = "李四"
}
运行打印输出:
原始Person的内存地址是:0xc4200721b0
函数里接收到Person的内存地址是:0xc4200721c0
{张三}
我们发现,我们自己定义的Person类型,在函数传参的时候也是值传递,但是它的值(Name字段)并没有被修改,我们想改成李四,发现最后的结果还是张三。
这也就是说,map类型和我们自己定义的struct类型是不一样的。我们尝试把modify函数的接收参数改为Person的指针。
func main() {
p:=Person{"张三"}
modify(&p)
fmt.Println(p)
}
type Person struct {
Name string
}
func modify(p *Person) {
p.Name = "李四"
}
在运行查看输出,我们发现,这次被修改了。我们这里省略了内存地址的打印,因为我们上面int类型的例子已经证明了指针类型的参数也是值传递的。 指针类型可以修改,非指针类型不行,那么我们可以大胆的猜测,我们使用make函数创建的map是不是一个指针类型呢?看一下源代码:
// makemap implements a Go map creation make(map[k]v, hint)
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If bucket != nil, bucket can be used as the first bucket.
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
//省略无关代码
}
通过查看src/runtime/hashmap.go源代码发现,的确和我们猜测的一样,make函数返回的是一个hmap类型的指针*hmap。也就是说map===*hmap。 现在看func modify(p map)这样的函数,其实就等于func modify(p *hmap),和我们前面第一节什么是值传递里举的func modify(ip *int)的例子一样,可以参考分析。
所以在这里,Go语言通过make函数,字面量的包装,为我们省去了指针的操作,让我们可以更容易的使用map。这里的map可以理解为引用类型,但是记住引用类型不是传引用。
chan类型
chan类型本质上和map类型是一样的,这里不做过多的介绍,参考下源代码:
func makechan(t *chantype, size int64) *hchan {
//省略无关代码
}
chan也是一个引用类型,和map相差无几,make返回的是一个*hchan。
和map、chan都不一样的slice
slice和map、chan都不太一样的,一样的是,它也是引用类型,它也可以在函数中修改对应的内容。
func main() {
ages:=[]int{6,6,6}
fmt.Printf("原始slice的内存地址是%p\n",ages)
modify(ages)
fmt.Println(ages)
}
func modify(ages []int){
fmt.Printf("函数里接收到slice的内存地址是%p\n",ages)
ages[0]=1
}
运行打印结果,发现的确是被修改了,而且我们这里打印slice的内存地址是可以直接通过%p打印的,不用使用&取地址符转换。
这就可以证明make的slice也是一个指针了吗?不一定,也可能fmt.Printf把slice特殊处理了。
func (p *pp) fmtPointer(value reflect.Value, verb rune) {
var u uintptr
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
u = value.Pointer()
default:
p.badVerb(verb)
return
}
//省略部分代码
}
通过源代码发现,对于chan、map、slice等被当成指针处理,通过value.Pointer()获取对应的值的指针。
// If v's Kind is Slice, the returned pointer is to the first
// element of the slice. If the slice is nil the returned value
// is 0. If the slice is empty but non-nil the return value is non-zero.
func (v Value) Pointer() uintptr {
// TODO: deprecate
k := v.kind()
switch k {
//省略无关代码
case Slice:
return (*SliceHeader)(v.ptr).Data
}
}
很明显了,当是slice类型的时候,返回是slice这个结构体里,字段Data第一个元素的地址。
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
type slice struct {
array unsafe.Pointer
len int
cap int
}
所以我们通过%p打印的slice变量ages的地址其实就是内部存储数组元素的地址,slice是一种结构体+元素指针的混合类型,通过元素array(Data)的指针,可以达到修改slice里存储元素的目的。
所以修改类型的内容的办法有很多种,类型本身作为指针可以,类型里有指针类型的字段也可以。
单纯的从slice这个结构体看,我们可以通过modify修改存储元素的内容,但是永远修改不了len和cap,因为他们只是一个拷贝,如果要修改,那就要传递*slice作为参数才可以。
func main() {
i:=19
p:=Person{name:"张三",age:&i}
fmt.Println(p)
modify(p)
fmt.Println(p)
}
type Person struct {
name string
age *int
}
func (p Person) String() string{
return "姓名为:" + p.name + ",年龄为:"+ strconv.Itoa(*p.age)
}
func modify(p Person){
p.name = "李四"
*p.age = 20
}
运行打印输出结果为:
姓名为:张三,年龄为:19
姓名为:张三,年龄为:20
通过这个Person和slice对比,就更好理解了,Person的name字段就类似于slice的len和cap字段,age字段类似于array字段。在传参为非指针类型的情况下,只能修改age字段,name字段无法修改。要修改name字段,就要把传参改为指针,比如:
modify(&p)
func modify(p *Person){
p.name = "李四"
*p.age = 20
}
这样name和age字段双双都被修改了。
所以slice类型也是引用类型。
小结
最终我们可以确认的是Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。
是否可以修改原内容数据,和传值、传引用没有必然的关系。在C++中,传引用肯定是可以修改原内容数据的,在Go语言里,虽然只有传值,但是我们也可以修改原内容数据,因为参数是引用类型。
这里也要记住,引用类型和传引用是两个概念。
再记住,Go里只有传值(值传递)。
来源:https://www.flysnow.org/2018/02/24/golang-function-parameters-passed-by-value.html


猜你喜欢
- XML Web Service 是在 Internet 上进行分布式计算的基本构造块。开放的标准以及对用户和应用程序之间的通信和协作的关注产
- 一、前言嗨,大家好,我是新发。最近需要做个小工具,可以通过python来读写Excel,实现增删改查操作。以前用的是xlrd和xlwt这两个
- Bootstrap是一款目前非常流行的前端框架,简单的说,就是html,css,javascript的工具集,我们可以用bootstrap搭
- ceil()方法返回x的值上限 - 不小于x的最小整数。语法以下是ceil()方法的语法:import mathmath.cei
- 什么是Flyway?转载:https://blog.waterstrong.me/flyway-in-practice/Flyway is
- 本文实例讲述了php实现的简单日志写入函数。分享给大家供大家参考。具体实现方法如下:function log( $logthis ){fil
- 本文实例为大家分享了微信小程序调用摄像头实现拍照的具体代码,供大家参考,具体内容如下微信小程序开发文档首先,需要用户授权摄像头权限,这一步是
- 引言:之前博文介绍过了mysql/oracle与ES之间的同步机制。而logstash最初始的日志同步功能还没有介绍。本文就logstash
- 本文实例讲述了Bootstrap简单实用的表单验证插件BootstrapValidator用法。分享给大家供大家参考,具体如下:Bootst
- 1 深分页问题1.1 创建表CREATE TABLE `player` ( `id` bigint(20) NOT NULL A
- 前言使用pandas对数据操作,筛选数据时,根据任务要求有时不仅要某列中存在空值的行,并且要删除某列中指定值所在行。1.data.dropn
- 这段时间在爬取了杭州某网站发布的二手房信息,在作图的时候发现在地图呈现上还是有欠缺,这里就把用到的贴出来,提升一下记忆。之前有接触用Base
- 本篇文章的python版本为:什么是httphttp是一个应用层协议,准确的来说是基于TCP/IP4层网络协议中的传输层中的TCP应用层协议
- 前言:集合这个词应该比较耳熟,大多数人没接触代码前就学过了。回想一下你的高一数学课本上是不是出现过这个词,就在第一章,概念如下:一般地,我们
- 本文记录了windows下python的安装,供大家参考,具体内容如下—–因为我是个真小白,网上的大多入门教程并不适合我这种超级超级小白,有
- python numpy 中linspace函数numpy提供linspace函数(有时也称为np.linspace)是python中创建数
- sys模块sys.argv: 实现从程序外部向程序传递参数。位置参数argv[0]代表py文件本身,运行方法 python xx.py 参数
- gjsonGJSON 是一个Go包,它提供了一种从json文档中获取值的快速简单的方法。它具有单行检索、点符号路径、迭代和解析 json 行
- 详解Python MD5加密Python 3下MD5加密# 由于MD5模块在python3中被移除# 在python3中使用hashlib模
- 1 前言在前面的章节中我们牛刀小试,一直在使用python爬虫去抓取数据,然后把数据信息存放在数据库中,至此已经完成了基本的基本信息的处理,