本文由云+社区发表javascript
几乎每个C++开发人员,都被面试过有关于函数参数是值传递仍是引用传递的问题,其实不止于C++,任何一个语言中,咱们都须要关心函数在参数传递时的行为。在golang中存在着map、channel和slice这三种内建数据类型,它们极大的方便着咱们的平常coding。然而,当这三种数据结构做为参数传递的时的行为是如何呢?本文将从这三个内建结构展开,来介绍golang中参数传递的一些细节问题。java
首先,咱们直接的来看一个简短的示例,下面几段代码的输出是什么呢?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变量地址居然同样。作技术开发的人都知道,在源代码世界中,若是地址同样,那就必然是同一个东西,也就是说:这俨然成为了一个引用传递的特性了。app
两个示例代码的结果居然截然相反,若是上述的内容让你产生了疑惑,而且你但愿完全的了解这过程当中发生了什么。那么请阅读完下面的内容,跟随做者一块儿从源码透过现象看本质。本文接下来的内容,将对golang中的map、channel和slice三种内建数据结构在做为函数参数传递时的行为进行分析,从而完整的解析golang中函数传递的行为。函数
Golang中的map,实际上就是一个hashtable,在这儿咱们不须要了解其详细的实现结构。回顾一下上文的例子咱们首先经过make()函数(运算符:=是make()的语法糖,相同的做用)初始化了一个map变量,而后将变量传递到test_map()中操做。ui
众所周知,在任何语言中,传递指针类型的参数才能够实如今函数内部直接修改内容,若是传递的是值自己的,会有一次拷贝发生(此时函数内外,该变量的地址会发生变化,经过第一个示例能够看出),所以,在函数内部的修改对原外部变量是无效的。可是,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
在介绍完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操做的同时,也会影响到函数外的数值。
对于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了另外一个元素,结果在函数外部:
其实这就是因为slice的结构引发的了。咱们都知道slice类型在make()的时候有个len和cap的可选参数,在上面的内部slice结构中第二和第三个成员变量就是表明着这俩个参数的含义。咱们已知缘由,数据部分因为是指针类型,这就决定了在函数内部对slice数据的修改是能够生效的,由于值传递进去的是指向数据的指针。而同一时刻,表示长度的len和容量的cap均为int类型,那么在传递到函数内部的就仅仅只是一个副本,所以在函数内部经过append修改了len的数值,但却影响不到函数外部slice的len变量,从而,append的影响便没法在函数外部看到了。
解释到这儿,基本说清了golang中map、channel和slice在函数传递时的行为和缘由了,可是,喜欢提问的读者可能一直以为有哪儿是怪怪的,这个时候咱们来完整的整理一下已经的关于slice的信息和行为:
没错了,对于问题一、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采用了引用传递,注意两者的区别哦~
此文已由做者受权腾讯云+社区在各渠道发布
获取更多新鲜技术干货,能够关注咱们腾讯云技术社区-云加社区官方号及知乎机构号