說說不知道的Golang中參數傳遞

本文由雲+社區發表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中函數傳遞的行爲。函數

迷惑人心的Map

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

跟風的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的信息和行爲:

  1. makeslice()出來的必定是個結構體對象,而不是指針;
  2. 函數內外打印的slice地址一致;
  3. 函數體內對slice中元素的修改在函數外部生效了;
  4. 函數體內對slice進行append操做在外部沒有生效;

沒錯了,對於問題一、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採用了引用傳遞,注意兩者的區別哦~

此文已由做者受權騰訊雲+社區在各渠道發佈

獲取更多新鮮技術乾貨,能夠關注咱們騰訊雲技術社區-雲加社區官方號及知乎機構號

相關文章
相關標籤/搜索