go語言函數參數傳遞類型詳解

到底是值傳遞還是引用傳遞?

golang默認都是值傳遞引用,無論參數是指針還是普通參數傳遞,默認都是值拷貝傳遞

什麼是值傳遞

函數傳遞的總是原來這個東西的一個副本,一副拷貝。比如我們傳遞一個int類型的參數,傳遞的其實是這個參數的一個副本;傳遞一個指針類型的參數,其實傳遞的是這個該指針的一份拷貝,而不是這個指針指向的值。

在網上看了一篇帖子,自己實際操作了進行驗證,以int類型的數據進行驗證,代碼如下

func modify(point *int) {
	fmt.Printf("函數裏接收到的指針的內存地址是:%p\n", &point)
	*point = 1
}

func funcB() {
	i := 10
	fmt.Printf("原始指針的內存地址是A:%p\n", &i)
	ip := &i
	fmt.Printf("原始指針的內存地址是:%p\n", &ip)
	modify(ip)
	fmt.Println("int值被修改了,新值爲:", i)
	fmt.Printf("原始指針的內存地址是B:%p\n", &i)
}

運行結果如下

原始指針的內存地址是A:0xc0000b4288
原始指針的內存地址是:0xc0000b0020
函數裏接收到的指針的內存地址是:0xc0000b0028
int值被修改了,新值爲: 1
原始指針的內存地址是B:0xc0000b4288

對於任何存放在內存裏面的東西,基本都是需要有內存地址,指針也是一樣的,需要存放指針地址,用來指向實際數據地址,從結果來看,雖然指針的值是相同的,但是指針的地址卻是不同的

在這裏插入圖片描述
這裏面的變量i,值爲10,系統申請的內存地址爲0xc0000b4288
指針ip也是一個指針類型的變量,它也需要內存存放它,它的內存地址是多少呢?是0xc0000b0020。 在我們傳遞指針變量ip給modify函數的時候,是該指針變量的拷貝,所以新拷貝的指針變量ip,它的內存地址已經變了,是新的0xc0000b0028。

不管是0xc0000b0020還是0xc0000b0028,我們都可以稱之爲指針的指針,他們指向同一個指針0xc0000b4288,這個0xc0000b4288又指向變量i,這也就是爲什麼我們可以修改變量i的值。

什麼是傳引用(引用傳遞)

Go語言(Golang)是沒有引用傳遞的

迷惑Map

瞭解清楚了傳值和傳引用,但是對於Map類型來說,可能覺得還是迷惑,一來我們可以通過方法修改它的內容,二來它沒有明顯的指針。

func main() {
	persons:=make(map[string]int)
	persons["張三"]=19

	mp:=&persons

	fmt.Printf("原始map的內存地址是:%p\n",mp)
	modify(persons)
	fmt.Println("map值被修改了,新值爲:",persons)
}

 func modify(p map[string]int){
	 fmt.Printf("函數裏接收到map的內存地址是:%p\n",&p)
	 p["張三"]=20
 }

打印輸出

原始map的內存地址是:0xc42000c028
函數裏接收到map的內存地址是:0xc42000c038
map值被修改了,新值爲: map[張三:20]

兩個內存地址是不一樣的,所以這又是一個值傳遞(值的拷貝),那麼爲什麼我們可以修改Map的內容呢?先不急,我們先看一個自己實現的struct。

func main() {
	p:=Person{"張三"}
	fmt.Printf("原始Person的內存地址是:%p\n",&p)
	modify(p)
	fmt.Println(p)
}

type Person struct {
	Name string
}

 func modify(p Person) {
	 fmt.Printf("函數裏接收到Person的內存地址是:%p\n",&p)
	 p.Name = "李四"
 }

運行打印輸出:

原始Person的內存地址是:0xc4200721b0
函數裏接收到Person的內存地址是:0xc4200721c0
{張三}

我們發現,我們自己定義的Person類型,在函數傳參的時候也是值傳遞,但是它的值(Name字段)並沒有被修改,我們想改成李四,發現最後的結果還是張三。

這也就是說,map類型和我們自己定義的struct類型是不一樣的。我們嘗試把modify函數的接收參數改爲Person的指針。

func main() {
	p:=Person{"張三"}
	modify(&p)
	fmt.Println(p)
}

type Person struct {
	Name string
}

 func modify(p *Person) {
	 p.Name = "李四"
 }

在運行查看輸出,我們發現,這次被修改了。我們這裏省略了內存地址的打印,因爲我們上面int類型的例子已經證明了指針類型的參數也是值傳遞的。 指針類型可以修改,非指針類型不行,那麼我們可以大膽的猜測,我們使用make函數創建的map是不是一個指針類型呢?看一下源代碼:

// makemap implements a Go map creation 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 bucket != nil, bucket can be used as the first bucket.
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
    //省略無關代碼
}```

通過查看src/runtime/hashmap.go源代碼發現,的確和我們猜測的一樣,make函數返回的是一個hmap類型的指針*hmap。也就是說map===*hmap。 現在看func modify(p map)這樣的函數,其實就等於func modify(p *hmap),和我們前面第一節什麼是值傳遞裏舉的func modify(ip *int)的例子一樣,可以參考分析。

所以在這裏,Go語言通過make函數,字面量的包裝,爲我們省去了指針的操作,讓我們可以更容易的使用map。這裏的map可以理解爲引用類型,但是記住引用類型不是傳引用。

chan類型

chan類型本質上和map類型是一樣的,這裏不做過多的介紹,參考下源代碼:

func makechan(t *chantype, size int64) *hchan {
    //省略無關代碼
}

chan也是一個引用類型,和map相差無幾,make返回的是一個*hchan。

和map、chan都不一樣的slice

slice和map、chan都不太一樣的,一樣的是,它也是引用類型,它也可以在函數中修改對應的內容。

func main() {
	ages:=[]int{6,6,6}
	fmt.Printf("原始slice的內存地址是%p\n",ages)
	modify(ages)
	fmt.Println(ages)
}

func modify(ages []int){
	fmt.Printf("函數裏接收到slice的內存地址是%p\n",ages)
	ages[0]=1
}

運行打印結果,發現的確是被修改了,而且我們這裏打印slice的內存地址是可以直接通過%p打印的,不用使用&取地址符轉換。

這就可以證明make的slice也是一個指針了嗎?不一定,也可能fmt.Printf把slice特殊處理了。

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
	}
	//省略部分代碼
}

通過源代碼發現,對於chan、map、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 Slice:
		return (*SliceHeader)(v.ptr).Data
	}
}

很明顯了,當是slice類型的時候,返回是slice這個結構體裏,字段Data第一個元素的地址。

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

所以我們通過%p打印的slice變量ages的地址其實就是內部存儲數組元素的地址,slice是一種結構體+元素指針的混合類型,通過元素array(Data)的指針,可以達到修改slice裏存儲元素的目的。

所以修改類型的內容的辦法有很多種,類型本身作爲指針可以,類型裏有指針類型的字段也可以。

單純的從slice這個結構體看,我們可以通過modify修改存儲元素的內容,但是永遠修改不了len和cap,因爲他們只是一個拷貝,如果要修改,那就要傳遞*slice作爲參數纔可以。

func main() {
	i:=19
	p:=Person{name:"張三",age:&i}
	fmt.Println(p)
	modify(p)
	fmt.Println(p)
}

type Person struct {
	name string
	age  *int
}

func (p Person) String() string{
	return "姓名爲:" + p.name + ",年齡爲:"+ strconv.Itoa(*p.age)
}

func modify(p Person){
	p.name = "李四"
	*p.age = 20
}

運行打印輸出結果爲:

姓名爲:張三,年齡爲:19
姓名爲:張三,年齡爲:20

通過這個Person和slice對比,就更好理解了,Person的name字段就類似於slice的len和cap字段,age字段類似於array字段。在傳參爲非指針類型的情況下,只能修改age字段,name字段無法修改。要修改name字段,就要把傳參改爲指針,比如:

modify(&p)
func modify(p *Person){
	p.name = "李四"
	*p.age = 20
}

這樣name和age字段雙雙都被修改了。

所以slice類型也是引用類型。

小結

最終我們可以確認的是Go語言中所有的傳參都是值傳遞(傳值),都是一個副本,一個拷貝。因爲拷貝的內容有時候是非引用類型(int、string、struct等這些),這樣就在函數中就無法修改原內容數據;有的是引用類型(指針、map、slice、chan等這些),這樣就可以修改原內容數據。

是否可以修改原內容數據,和傳值、傳引用沒有必然的關係。在C++中,傳引用肯定是可以修改原內容數據的,在Go語言裏,雖然只有傳值,但是我們也可以修改原內容數據,因爲參數是引用類型。

這裏也要記住,引用類型和傳引用是兩個概念。

再記住,Go裏只有傳值(值傳遞)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章