本文由雲+社區發表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採用了引用傳遞,注意兩者的區別哦~
此文已由做者受權騰訊雲+社區在各渠道發佈
獲取更多新鮮技術乾貨,能夠關注咱們騰訊雲技術社區-雲加社區官方號及知乎機構號