哈嘍,你們好,我是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
,我拉你入羣。歡迎各位的關注,咱們下期見~~~
推薦往期文章: