哈喽,你们好,我是asong。今天女友问我,小松子,你知道Go语言参数传递是传值仍是传引用吗?哎呀哈,我居然被瞧不起了,我立马一顿操做,给他讲的明明白白的,小丫头片子,仍是太嫩,你们且听我细细道来~~~。
咱们使用go
定义方法时是能够定义参数的。好比以下方法:golang
func printNumber(args ...int)
这里的args
就是参数。参数在程序语言中分为形式参数和实际参数。面试
形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。设计模式
实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。数组
举例以下:缓存
func main() { var args int64= 1 printNumber(args) // args就是实际参数 } func printNumber(args ...int64) { //这里定义的args就是形式参数 for _,arg := range args{ fmt.Println(arg) } }
值传递,咱们分析其字面意思:传递的就是值。传值的意思是:函数传递的老是原来这个东西的一个副本,一副拷贝。好比咱们传递一个int
类型的参数,传递的实际上是这个参数的一个副本;传递一个指针类型的参数,其实传递的是这个该指针的一份拷贝,而不是这个指针指向的值。咱们画个图来解释一下:架构
学习过其余语言的同窗,对这个引用传递应该很熟悉,好比C++
使用者,在C++中,函数参数的传递方式有引用传递。所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。app
咱们先写一个简单的例子验证一下:异步
func main() { var args int64= 1 modifiedNumber(args) // args就是实际参数 fmt.Printf("实际参数的地址 %p\n", &args) fmt.Printf("改动后的值是 %d\n",args) } func modifiedNumber(args int64) { //这里定义的args就是形式参数 fmt.Printf("形参地址 %p \n",&args) args = 10 }
运行结果:函数
形参地址 0xc0000b4010 实际参数的地址 0xc0000b4008 改动后的值是 1
这里正好验证了go
是值传递,可是还不能彻底肯定go
就只有值传递,咱们在写一个例子验证一下:微服务
func main() { var args int64= 1 addr := &args fmt.Printf("原始指针的内存地址是 %p\n", addr) fmt.Printf("指针变量addr存放的地址 %p\n", &addr) modifiedNumber(addr) // args就是实际参数 fmt.Printf("改动后的值是 %d\n",args) } func modifiedNumber(addr *int64) { //这里定义的args就是形式参数 fmt.Printf("形参地址 %p \n",&addr) *addr = 10 }
运行结果:
原始指针的内存地址是 0xc0000b4008 指针变量addr存放的地址 0xc0000ae018 形参地址 0xc0000ae028 改动后的值是 10
因此经过输出咱们能够看到,这是一个指针的拷贝,由于存放这两个指针的内存地址是不一样的,虽然指针的值相同,可是是两个不一样的指针。
经过上面的图,咱们能够更好的理解。咱们声明了一个变量args
,其值为1
,而且他的内存存放地址是0xc0000b4008
,经过这个地址,咱们就能够找到变量args
,这个地址也就是变量args
的指针addr
。指针addr
也是一个指针类型的变量,它也须要内存存放它,它的内存地址是多少呢?是0xc0000ae018
。 在咱们传递指针变量addr
给modifiedNumber
函数的时候,是该指针变量的拷贝,因此新拷贝的指针变量addr
,它的内存地址已经变了,是新的0xc0000ae028
。因此,不论是0xc0000ae018
仍是0xc0000ae028
,咱们均可以称之为指针的指针,他们指向同一个指针0xc0000b4008
,这个0xc0000b4008
又指向变量args
,这也就是为何咱们能够修改变量args
的值。
经过上面的分析,咱们就能够肯定go
就是值传递,由于咱们在modifieNumber
方法中打印出来的内存地址发生了改变,因此不是引用传递,实锤了奥兄弟们,证据确凿~~~。等等,好像好落下了点什么,说好的go中只有值传递呢,为何chan
、map
、slice
类型传递却能够改变其中的值呢?白着急,咱们依次来验证一下。
slice
也是值传递吗?先看一段代码:
func main() { var args = []int64{1,2,3} fmt.Printf("切片args的地址: %p\n",args) modifiedNumber(args) fmt.Println(args) } func modifiedNumber(args []int64) { fmt.Printf("形参切片的地址 %p \n",args) args[0] = 10 }
运行结果:
切片args的地址: 0xc0000b8000 形参切片的地址 0xc0000b8000 [10 2 3]
哇去,怎么回事,光速打脸呢,这怎么地址都是同样的呢?而且值还被修改了呢?怎么回事,做何解释,你个渣男,欺骗我感情。。。很差意思走错片场了。继续来看这个问题。这里咱们没有使用&
符号取地址符转换,就把slice
地址打印出来了,咱们在加上一行代码测试一下:
func main() { var args = []int64{1,2,3} fmt.Printf("切片args的地址: %p \n",args) fmt.Printf("切片args第一个元素的地址: %p \n",&args[0]) fmt.Printf("直接对切片args取地址%v \n",&args) modifiedNumber(args) fmt.Println(args) } func modifiedNumber(args []int64) { fmt.Printf("形参切片的地址 %p \n",args) fmt.Printf("形参切片args第一个元素的地址: %p \n",&args[0]) fmt.Printf("直接对形参切片args取地址%v \n",&args) args[0] = 10 }
运行结果:
切片args的地址: 0xc000016140 切片args第一个元素的地址: 0xc000016140 直接对切片args取地址&[1 2 3] 形参切片的地址 0xc000016140 形参切片args第一个元素的地址: 0xc000016140 直接对形参切片args取地址&[1 2 3] [10 2 3]
经过这个例子咱们能够看到,使用&操做符表示slice的地址是无效的,并且使用%p输出的内存地址与slice的第一个元素的地址是同样的,那么为何会出现这样的状况呢?会不会是fmt.Printf
函数作了什么特殊处理?咱们来看一下其源码:
fmt包,print.go中的printValue这个方法,截取重点部分,由于`slice`也是引用类型,因此会进入这个`case`: case reflect.Ptr: // pointer to array or slice or struct? ok at top level // but not embedded (avoid loops) if depth == 0 && f.Pointer() != 0 { switch a := f.Elem(); a.Kind() { case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map: p.buf.writeByte('&') p.printValue(a, verb, depth+1) return } } fallthrough case reflect.Chan, reflect.Func, reflect.UnsafePointer: p.fmtPointer(f, verb)
p.buf.writeByte('&')
这行代码就是为何咱们使用&
打印地址输出结果前面带有&
的语音。由于咱们要打印的是一个slice
类型,就会调用p.printValue(a, verb, depth+1)
递归获取切片中的内容,为何打印出来的切片中还会有[]
包围呢,我来看一下printValue
这个方法的源代码:
case reflect.Array, reflect.Slice: //省略部分代码 } else { p.buf.writeByte('[') for i := 0; i < f.Len(); i++ { if i > 0 { p.buf.writeByte(' ') } p.printValue(f.Index(i), verb, depth+1) } p.buf.writeByte(']') }
这就是上面 fmt.Printf("直接对切片args取地址%v \\n",&args)
输出直接对切片args取地址&[1 2 3]
的缘由。这个问题解决了,咱们再来看一看使用%p输出的内存地址与slice的第一个元素的地址是同样的。在上面的源码中,有这样一行代码fallthrough
,表明着接下来的fmt.Poniter
也会被执行,我看一下其源码:
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 } ...... 省略部分代码 // 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: if v.flag&flagMethod != 0 { ....... 省略部分代码
这里咱们能够看到上面有这样一句注释:If v's Kind is Slice, the returned pointer is to the first。翻译成中文就是若是是slice
类型,返回slice
这个结构里的第一个元素的地址。这里正好解释上面为何fmt.Printf("切片args的地址: %p \\n",args)
和fmt.Printf("形参切片的地址 %p \\n",args)
打印出来的地址是同样的,由于args
是引用类型,因此他们都返回slice
这个结构里的第一个元素的地址,为何这两个slice
结构里的第一个元素的地址同样呢,这就要在说一说slice
的底层结构了。
咱们看一下slice
底层结构:
//runtime/slice.go type slice struct { array unsafe.Pointer len int cap int }
slice
是一个结构体,他的第一个元素是一个指针类型,这个指针指向的是底层数组的第一个元素。因此当是slice
类型的时候,fmt.Printf
返回是slice
这个结构体里第一个元素的地址。说到底,又转变成了指针处理,只不过这个指针是slice
中第一个元素的内存地址。
说了这么多,最后再作一个总结吧,为何slice
也是值传递。之因此对于引用类型的传递能够修改原内容的数据,这是由于在底层默认使用该引用类型的指针进行传递,但也是使用指针的副本,依旧是值传递。因此slice
传递的就是第一个元素的指针的副本,由于fmt.printf
缘故形成了打印的地址同样,给人一种混淆的感受。
map
和slice
同样都具备迷惑行为,哼,渣女。map
咱们能够经过方法修改它的内容,而且它没有明显的指针。好比这个例子:
func main() { persons:=make(map[string]int) persons["asong"]=8 addr:=&persons fmt.Printf("原始map的内存地址是:%p\n",addr) modifiedAge(persons) fmt.Println("map值被修改了,新值为:",persons) } func modifiedAge(person map[string]int) { fmt.Printf("函数里接收到map的内存地址是:%p\n",&person) person["asong"]=9 }
看一眼运行结果:
原始map的内存地址是:0xc00000e028 函数里接收到map的内存地址是:0xc00000e038 map值被修改了,新值为: map[asong:9]
先喵一眼,哎呀,实参与形参地址不同,应该是值传递无疑了,等等。。。。map
值怎么被修改了?一脸疑惑。。。。。
为了解决咱们的疑惑,咱们从源码入手,看一看什么原理:
//src/runtime/map.go // 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 { mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size) if overflow || mem > maxAlloc { hint = 0 } // initialize Hmap if h == nil { h = new(hmap) } h.hash0 = fastrand()
从以上源码,咱们能够看出,使用make
函数返回的是一个hmap
类型的指针*hmap
。回到上面那个例子,咱们的func modifiedAge(person map[string]int)
函数,其实就等于func modifiedAge(person *hmap)
,实际上在做为传递参数时仍是使用了指针的副本进行传递,属于值传递。在这里,Go语言经过make
函数,字面量的包装,为咱们省去了指针的操做,让咱们能够更容易的使用map。这里的map
能够理解为引用类型,可是记住引用类型不是传引用。
老样子,先看一个例子:
func main() { p:=make(chan bool) fmt.Printf("原始chan的内存地址是:%p\n",&p) go func(p chan bool){ fmt.Printf("函数里接收到chan的内存地址是:%p\n",&p) //模拟耗时 time.Sleep(2*time.Second) p<-true }(p) select { case l := <- p: fmt.Println(l) } }
再看一看运行结果:
原始chan的内存地址是:0xc00000e028 函数里接收到chan的内存地址是:0xc00000e038 true
这个怎么回事,实参与形参地址不同,可是这个值是怎么传回来的,说好的值传递呢?白着急,铁子,咱们像分析map
那样,再来分析一下chan
。首先看源码:
// src/runtime/chan.go func makechan(t *chantype, size int) *hchan { elem := t.elem // compiler checks this but be safe. if elem.size >= 1<<16 { throw("makechan: invalid channel element type") } if hchanSize%maxAlign != 0 || elem.align > maxAlign { throw("makechan: bad alignment") } mem, overflow := math.MulUintptr(elem.size, uintptr(size)) if overflow || mem > maxAlloc-hchanSize || size < 0 { panic(plainError("makechan: size out of range")) }
从以上源码,咱们能够看出,使用make
函数返回的是一个hchan
类型的指针*hchan
。这不是与map
一个道理嘛,再次回到上面的例子,实际咱们的fun (p chan bool)
与fun (p *hchan)
是同样的,实际上在做为传递参数时仍是使用了指针的副本进行传递,属于值传递。
是否是到这里,基本就能够肯定go
就是值传递了呢?还剩最后一个没有测试,那就是struct
,咱们最后来验证一下struct
。
struct
就是值传递没错,我先说答案,struct
就是值传递,不信你看这个例子:
func main() { per := Person{ Name: "asong", Age: int64(8), } fmt.Printf("原始struct地址是:%p\n",&per) modifiedAge(per) fmt.Println(per) } func modifiedAge(per Person) { fmt.Printf("函数里接收到struct的内存地址是:%p\n",&per) per.Age = 10 }
咱们发现,咱们本身定义的Person
类型,在函数传参的时候也是值传递,可是它的值(Age
字段)并无被修改,咱们想改为10
,发现最后的结果仍是8
。
兄弟们实锤了奥,go就是值传递,能够确认的是Go语言中全部的传参都是值传递(传值),都是一个副本,一个拷贝。由于拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就没法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就能够修改原内容数据。
是否能够修改原内容数据,和传值、传引用没有必然的关系。在C++中,传引用确定是能够修改原内容数据的,在Go语言里,虽然只有传值,可是咱们也能够修改原内容数据,由于参数是引用类型。
有的小伙伴会在这里仍是懵逼,由于你把引用类型和传引用当成一个概念了,这是两个概念,切记!!!
欢迎在评论区留下你的答案~~~
既然大家都知道了golang只有值传递,那么这段代码来帮我分析一下吧,这里的值能修改为功,为何使用append不会发生扩容?
func main() { array := []int{7,8,9} fmt.Printf("main ap brfore: len: %d cap:%d data:%+v\n", len(array), cap(array), array) ap(array) fmt.Printf("main ap after: len: %d cap:%d data:%+v\n", len(array), cap(array), array) } func ap(array []int) { fmt.Printf("ap brfore: len: %d cap:%d data:%+v\n", len(array), cap(array), array) array[0] = 1 array = append(array, 10) fmt.Printf("ap after: len: %d cap:%d data:%+v\n", len(array), cap(array), array) }
好啦,这一篇文章到这就结束了,咱们下期见~~。但愿对大家有用,又不对的地方欢迎指出,可添加个人golang交流群,咱们一块儿学习交流。
结尾给你们发一个小福利吧,最近我在看[微服务架构设计模式]这一本书,讲的很好,本身也收集了一本PDF,有须要的小伙能够到自行下载。获取方式:关注公众号:[Golang梦工厂],后台回复:[微服务],便可获取。
我翻译了一份GIN中文文档,会按期进行维护,有须要的小伙伴后台回复[gin]便可下载。
翻译了一份Machinery中文文档,会按期进行维护,有须要的小伙伴们后台回复[machinery]便可获取。
我是asong,一名普普统统的程序猿,让gi我一块儿慢慢变强吧。我本身建了一个golang
交流群,有须要的小伙伴加我vx
,我拉你入群。欢迎各位的关注,咱们下期见~~~
推荐往期文章: