说说不知道的Golang中参数传递
本文由云+社区发表
导言
几乎每一个C++开发人员,都被面试过有关于函数参数是值传递还是引用传递的问题,其实不止于C++,任何一个语言中,我们都需要关心函数在参数传递时的行为。在golang中存在着map、channel和slice这三种内建数据类型,它们极大的方便着我们的日常coding。然而,当这三种数据结构作为参数传递的时的行为是如何呢?本文将从这三个内建结构展开,来介绍golang中参数传递的一些细节问题。
背景
首先,我们直接的来看一个简短的示例,下面几段代码的输出是什么呢?
//demo1
package main
import "fmt"
func test_string(s string){
fmt.Printf("inner: %v, %v\n",s, &s)
s = "b"
fmt.Printf("inner: %v, %v\n",s, &s)
}
func main() {
s := "a"
fmt.Printf("outer: %v, %v\n",s, &s)
test_string(s)
fmt.Printf("outer: %v, %v\n",s, &s)
}
上文的代码段,尝试在函数test_string()内部修改一个字符串的数值,通过运行结果,我们可以清楚的看到函数test_string()中入参的指针地址发生了变化,且函数外部变量未被内部的修改所影响。因此,很直接的一个结论呼之欲出:golang中函数的参数传递采用的是:值传递。
//output
outer: a, 0x40e128
inner: a, 0x40e140
inner: b, 0x40e140
outer: a, 0x40e128
那么是不是到这儿就回答完,本文就结束了呢?当然不是,请再请看看下面的例子:当我们使用的参数不再是string,而改为map类型传入时,输出结果又是什么呢?
//demo2
package main
import "fmt"
func test_map(m map[string]string){
fmt.Printf("inner: %v, %p\n",m, m)
m["a"]="11"
fmt.Printf("inner: %v, %p\n",m, m)
}
func main() {
m := map[string]string{
"a":"1",
"b":"2",
"c":"3",
}
fmt.Printf("outer: %v, %p\n",m, m)
test_map(m)
fmt.Printf("outer: %v, %p\n",m, m)
}
根据我们前文得出的结论,按照值传递的特性,我们毫无疑问的猜想:函数外两次输出的结果应该是相同的,同时地址应该不同。然而,事实却正是相反:
//output
outer: map[a:1 b:2 c:3], 0x442260
inner: map[a:1 b:2 c:3], 0x442260
inner: map[a:11 b:2 c:3], 0x442260
outer: map[b:2 c:3 a:11], 0x442260
没错,在函数test_map()中对map的修改再函数外部生效了,而且函数内外打印的map变量地址竟然一样。做技术开发的人都知道,在源代码世界中,如果地址一样,那就必然是同一个东西,也就是说:这俨然成为了一个引用传递的特性了。
两个示例代码的结果竟然截然相反,如果上述的内容让你产生了疑惑,并且你希望彻底的了解这过程中发生了什么。那么请阅读完下面的内容,跟随作者一起从源码透过现象看本质。本文接下来的内容,将对golang中的map、channel和slice三种内建数据结构在作为函数参数传递时的行为进行分析,从而完整的解析golang中函数传递的行为。
迷惑人心的Map
Golang中的map,实际上就是一个hashtable,在这儿我们不需要了解其详细的实现结构。回顾一下上文的例子我们首先通过make()函数(运算符:=是make()的语法糖,相同的作用)初始化了一个map变量,然后将变量传递到test_map()中操作。
众所周知,在任何语言中,传递指针类型的参数才可以实现在函数内部直接修改内容,如果传递的是值本身的,会有一次拷贝发生(此时函数内外,该变量的地址会发生变化,通过第一个示例可以看出),因此,在函数内部的修改对原外部变量是无效的。但是,demo2示例中的变量却完全没有拷贝发生的迹象,那么,我们是否可以大胆的猜测,通过make()函数创建出来的map变量会不会实际上是一个指针类型呢?这时候,我们便需要来看一下源代码了:
// makemap implements Go map creation for 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 h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
if hint < 0 || hint > int(maxSliceCap(t.bucket.size)) {
hint = 0
}
...
上面是golang中的make()函数在map中通过makemap()函数来实现的代码段,可以看到,与我们猜测一致的是:makemap()返回的是一个hmap类型的指针hmap。也就是说:test_map(map)实际上等同于test_map(hmap)。因此,在golang中,当map作为形参时,虽然是值传递,但是由于make()返回的是一个指针类型,所以我们可以在函数哪修改map的数值并影响到函数外。
我们也可以通过一个不是很恰当的反例来证明这点:
//demo3
package main
import "fmt"
func test_map2(m map[string]string){
fmt.Printf("inner: %v, %p\n",m, m)
m = make(map[string]string, 0)
m["a"]="11"
fmt.Printf("inner: %v, %p\n",m, m)
}
func main() {
var m map[string]string//未初始化
fmt.Printf("outer: %v, %p\n",m, m)
test_map2(m)
fmt.Printf("outer: %v, %p\n",m, m)
}
由于在函数test_map2()外仅仅对map变量m进行了声明而未初始化,在函数test_map2()中才对map进行了初始化和赋值操纵,这时候,我们看到对于map的更改便无法反馈到函数外了。
//output
outer: map[], 0x0
inner: map[], 0x0
inner: map[a:11], 0x442260
outer: map[], 0x0
跟风的Channel
在介绍完map类型作为参数传递时的行为后,我们再来看看golang的特殊类型:channel的行为。还是通过一段代码来来入手:
//demo4
package main
import "fmt"
func test_chan2(ch chan string){
fmt.Printf("inner: %v, %v\n",ch, len(ch))
ch<-"b"
fmt.Printf("inner: %v, %v\n",ch, len(ch))
}
func main() {
ch := make(chan string, 10)
ch<- "a"
fmt.Printf("outer: %v, %v\n",ch, len(ch))
test_chan2(ch)
fmt.Printf("outer: %v, %v\n",ch, len(ch))
}
结果如下,我们看到,在函数内往channel中塞入数值,在函数外可以看到channel的size发生了变化:
//output
outer: 0x436100, 1
inner: 0x436100, 1
inner: 0x436100, 2
outer: 0x436100, 2
在golang中,对于channel有着与map类似的结果,其make()函数实现源代码如下:
func makechan(t *chantype, size int) *hchan {
elem := t.elem
...
也就是make() chan的返回值为一个hchan类型的指针,因此当我们的业务代码在函数内对channel操作的同时,也会影响到函数外的数值。
与众不同的Slice
对于golang中slice的行为,可以总结一句话:与众不同。首先,我们来看下golang中对于slice的make实现代码:
func makeslice(et *_type, len, cap int) slice {
...
我们发现,与map和channel不同的是,sclie的make函数返回的是一个内建结构体类型slice的对象,而并非一个指针类型,其中内建slice的数据结构如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
也就是说,如果采用slice在golang中传递参数,在函数内对slice的操作是不应该影响到函数外的。那么,对于下面的这段示例代码,运行的结果又是什么呢?
//demo5
package main
import "fmt"
func main() {
sl := []string{
"a",
"b",
"c",
}
fmt.Printf("%v, %p\n",sl, sl)
test_slice(sl)
fmt.Printf("%v, %p\n",sl, sl)
}
func test_slice(sl []string){
fmt.Printf("%v, %p\n",sl, sl)
sl[0] = "aa"
//sl = append(sl, "d")
fmt.Printf("%v, %p\n",sl, sl)
}
通过运行结果,我们看到,在函数内部对slice中的第一个元素的数值修改成功的返回到了test_slice()函数外层!与此同时,通过打印地址,我们发现也显示了是同一个地址。到了这儿,似乎又一个奇怪的现象出现了:makeslice()返回的是值类型,但是当该数值作为参数传递时,在函数内外的地址却未发生变化,俨然一副指针类型。
//output
[a b c], 0x442260
[a b c], 0x442260
[aa b c], 0x442260
[aa b c], 0x442260
这时候,我们还是回归源码,回顾一下上面列出的golang内部slice结构体的特点。没错,细心地读者可能已经发现,内部slice中的第一个元素用来存放数据的结构是个指针类型,一个指向了真正的存放数据的指针!因此,虽然指针拷贝了,但是指针所指向的地址却未更改,而我们在函数内部修改了指针所指向的地方的内容,从而实现了对元素修改的目的了。
让我们再进阶一下上面的示例,将注释的那行代码打开:
sl = append(sl, "d")
再重新运行上面的代码,得到的结果又有了新的变化:
//output
[a b c], 0x442280
[a b c], 0x442280
[aa b c d], 0x442280
[aa b c], 0x442280
函数内我们修改了slice中一个已有元素,同时向slice中append了另一个元素,结果在函数外部:
- 修改的元素生效了;
- append的元素却消失了。
其实这就是由于slice的结构引起的了。我们都知道slice类型在make()的时候有个len和cap的可选参数,在上面的内部slice结构中第二和第三个成员变量就是代表着这俩个参数的含义。我们已知原因,数据部分由于是指针类型,这就决定了在函数内部对slice数据的修改是可以生效的,因为值传递进去的是指向数据的指针。而同一时刻,表示长度的len和容量的cap均为int类型,那么在传递到函数内部的就仅仅只是一个副本,因此在函数内部通过append修改了len的数值,但却影响不到函数外部slice的len变量,从而,append的影响便无法在函数外部看到了。
解释到这儿,基本说清了golang中map、channel和slice在函数传递时的行为和原因了,但是,喜欢提问的读者可能一直觉得有哪儿是怪怪的,这个时候我们来完整的整理一下已经的关于slice的信息和行为:
- makeslice()出来的一定是个结构体对象,而不是指针;
- 函数内外打印的slice地址一致;
- 函数体内对slice中元素的修改在函数外部生效了;
- 函数体内对slice进行append操作在外部没有生效;
没错了,对于问题1、3和4我们应该都已经解释清楚了,但是,关于第2点为什么函数内外对于这三个内建类型变量的地址打印却是一致的?我们已经更加确定了golang中的参数传递的确是值类型,那么,造成这一现象的唯一可能就是出在打印函数fmt.Printf()中有些小操作了。因为我们是通过%p来打印地址信息的,为此,我们需要关注的是fmt包中fmtPointer():
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
}
...
}
我们发现在fmtPointer()中,对于map、channel和slice,都被当成了指针来处理,通过Pointer()函数获取对应的值的指针。我们知道channel和map是因为make函数返回的就已经是指针了,无可厚非,但是对于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 Chan, Map, Ptr, UnsafePointer:
return uintptr(v.pointer())
case Func:
...
case Slice:
return (*SliceHeader)(v.ptr).Data
}
...
}
果不其然,在Pointer()函数中,对于Slice类型的数据,返回的一直是指向第一个元素的地址,所以我们通过fmt.Printf()中%p来打印Slice的地址,其实打印的结果是内部存储数组元素的首地址,这也就解释了问题2中为什么地址会一致的原因了。
总结
通过上述的一系列总结,我们可以很高兴的确定的是:在golang中的传参一定是值传递了!
然而golang隐藏了一些实现细节,在处理map,channel和slice等这些内置结构的数据时,其实处理的是一个指针类型的数据,也是因此,在函数内部可以修改(部分修改)数据的内容。
但是,这些修改得以实现的原因,是因为数据本身是个指针类型,而不是因为golang采用了引用传递,注意二者的区别哦~
此文已由作者授权腾讯云+社区在各渠道发布
获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号
说说不知道的Golang中参数传递的更多相关文章
- 你可能不知道的 JavaScript 中数字取整
网上方法很多,标题党一下,勿拍 ^_^!实际开发过程中经常遇到数字取整问题,所以这篇文章收集了一些方法,以备查询. 常用的直接取整方法 直接取整就是舍去小数部分. 1.parseInt() parse ...
- 你所不知道的 C# 中的细节
前言 有一个东西叫做鸭子类型,所谓鸭子类型就是,只要一个东西表现得像鸭子那么就能推出这玩意就是鸭子. C# 里面其实也暗藏了很多类似鸭子类型的东西,但是很多开发者并不知道,因此也就没法好好利用这些东西 ...
- 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制
你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...
- Android中Context详解 ---- 你所不知道的Context
转自:http://blog.csdn.net/qinjuning/article/details/7310620Android中Context详解 ---- 你所不知道的Context 大家好, ...
- 你所不知道的html5与html中的那些事第三篇
文章简介: 关于html5相信大家早已经耳熟能详,但是他真正的意义在具体的开发中会有什么作用呢?相对于html,他又有怎样的新的定义与新理念在里面呢?为什么一些专家认为html5完全完成后,所有的工作 ...
- JavaScript中你可能不知道的九件事
今天凑巧去W3School扫了一遍JavaScript教程,发现从中看到了不少自己曾经没有注意过的细节. 我这些细节列在这里.分享给可能相同不知道的朋友: 1.使用 document.write() ...
- JavaScript中你所不知道的Object(二)--Function篇
上一篇(JavaScript中你所不知道的Object(一))说到,Object对象有大量的内部属性,而其中多数和外部属性的操作有关.最后留了个悬念,就是Boolean.Date.Number.Str ...
- Spring中你可能不知道的事(一)
Spring作为Java的王牌开源项目,相信大家都用过,但是可能大家仅仅用到了Spring最常用的功能,Spring实在是庞大了,很多功能可能一辈子都不会用到,今天我就罗列下Spring中你可能不知道 ...
- [No0000194]聊聊 Chrome DevTools 中你可能不知道的调试技巧
对于前端开发者来说,ChromeDevTools 绝对是不可或缺的调试工具,我们常用的调试方法包含一些console等,而ChromeDevTools 其实很强大,下面来聊聊一些你可能不知道的debu ...
随机推荐
- 如何使用yql实现跨域访问
应用场景 调用百度的某个API, 例如:https://openapi.baidu.com/api 返回结果是:{"id":123,"name":"t ...
- VB输出数据到EXCEL
Private Sub Command1_Click() Dim i As Long Dim j As Long , ) As Long Dim xlApp, WS, WB Set xlApp = C ...
- css实现中文换行,英文换行,超出省略
英文换行时,是以单词换行,在对应的标签添加对应的属性即可 1 word-break:break-all;只对英文起作用,以字母作为换行依据 2 word-wrap:break-word; 只对英文起作 ...
- PeopleSoft 启用多语言输入
今天,我的一位同事询问有的人有语言选择,有的人没有(如下图所示). 以下是PT856.09菜单路经: 主菜单-->我的首选项--->一般设置--->多语言条目
- 如何使用微信小程序云函数发送短信验证码
其实微信小程序前端和云端都是可以调用短信平台接口发送短信的,使用云端云函数的好处是无需配置域名,也没有个数限制. 本文使用的是榛子云短信平台(http://smsow.zhenzikj.com) ,S ...
- H5音乐自动播放ios//禁止安卓手机图片点击
定义音乐按钮 <div id="music-btn" class="o-play" style="width: 24px; height: 24 ...
- Mesos源码分析(9): Test Framework的启动
我们以Test Framework为例子解释Framework的启动方式. Test Framework的代码在src/examples/test_framework.cpp中的main函数 首先要指 ...
- ReactJs和React Native的联系和差异
1,React Js的目的 是为了使前端的V层更具组件化,能更好的复用,它能够使用简单的html标签创建更多的自定义组件标签,内部绑定事件,同时可以让你从操作dom中解脱出来,只需要操作数据就会改变相 ...
- 老司机带路:《axios从入门到开车 嘀嘀~~》
前言:axios vue.axios 跨域.axios.js.axios get.axios post.axios中文文档 之前当vue更新到2.0之后,作者就宣告不再对vue-resource更新, ...
- 吴恩达机器学习笔记57-基于内容的推荐系统(Content Based Recommendations)
假使我们是一个电影供应商,我们有 5 部电影和 4 个用户,我们要求用户为电影打分. 前三部电影是爱情片,后两部则是动作片,我们可以看出Alice 和Bob 似乎更倾向与爱情片, 而 Carol 和 ...