什么是傳值(值傳遞)?
傳值的意思是:函數傳遞的總是原來這個東西的一個副本、一個副拷貝。比如我們傳遞一個 int 類型的參數,傳遞
的其實這個參數的一個副本;傳遞一個指針類型的參數,其實傳遞的是這個指針的一份拷貝,而不是這個指針指向的
值
對於 int 這類基礎類型的我們可以很容易理解,它們就是一個拷貝,但是指針呢?我們可以通過它修改原來的值,怎
么會是一個拷貝呢?看如下示例:
package main import "fmt" func modify(ip *int) { fmt.Printf("函數里接收指針的內存地址是:%p\n",&ip) *ip = 1 } func main() { i := 10 ip := &i fmt.Printf("原始指針的內存地址是:%p\n",&ip) modify(ip) fmt.Println("int 值被修改了,新值是:",i) }
輸出結果如下:
原始指針的內存地址是:0xc00007e018 函數里接收指針的內存地址是:0xc00007e028 int 值被修改了,新值是: 1
我們都知道,任何存放在內存里的東西都有自己的地址,指針也不例外,它雖然指向別的數據,但是也有存放該指
針的內存
首先,我們聲明了一個變量 i,值為 10,它的內存存放地址是 0xc000056080,通過這個地址我們可以找到變量 i,
這個內存地址也就是變量 i 的指針 ip,指針 ip 是一個指針類型的變量,它也需要內存存放它,它的內存地址是 0xc0000
7e018,在我們傳遞指針變量 ip 給 modify 函數的時候,是對這個指針變量的一個拷貝,所以新拷貝的指針變量 ip 它的內
存地址就變了,是新的 0xc00007e028,不管是 0xc00007e018 還是 xc00007e028 我們都可以稱之為指針的指針,他們
指向同一個指針 0xc000056080,這個 0xc000056080 又指向 變量 i,這也就是為什么我們可以修改變量 i 的值
什么是引用傳遞?
在 Go 語言中沒有引用傳遞,以上面的例子為例,如果在 modify 函數里面打印的地址不變,那么就是引用傳遞
迷惑 Map
了解清楚了值傳遞和引用傳遞,但是對於 Map 類型來說,可能覺得還是迷惑,一個是我們可以通過方法修改它的內容,
二個是它沒有明顯的指針,如下示例:
package main import "fmt" func modify(p map[string]int) { fmt.Printf("函數里接收到的map內存地址:%p\n",&p) p["張三"] = 25 } func main() { // 聲明一個 map persons := make(map[string]int) persons["張三"] = 19 mp := &persons fmt.Printf("原始map的內存地址是:%p\n",mp) modify(persons) fmt.Println("map值被修改了,新值是:",persons) }
運行打印輸出結果:
原始map的內存地址是:0xc000006028 函數里接收到的map內存地址:0xc000006038 map值被修改了,新值是: map[張三:25]
兩個內存地址不一樣,說明是值傳遞,那么為什么是值傳遞我們還可以修改 Map 的內容呢?我們先看一個
自己實現的 struct ,如下示例:
package main import "fmt" // 聲明一個結構體 type Person struct { Name string } func modify(p Person) { fmt.Printf("函數里接收到的Person內存地址:%p\n",&p) p.Name = "李四" } func main() { p := Person{"張三"} fmt.Printf("原始Person的內存地址是:%p\n",&p) modify(p) fmt.Println(p) }
運行打印輸出:
原始Person的內存地址是:0xc00004a1c0 函數里接收到的Person內存地址:0xc00004a1d0 {張三}
我們看到,自己定義的 Person 類型,在傳遞參數的時候也是值傳遞,但是它的值並沒有被修改,這也就
是說 map 類型 和我們自己定義 struct 類型是不一樣的,我們把 modify 函數接收的參數改為 Person 的指針:
package main import "fmt" // 聲明一個結構體 type Person struct { Name string } func modify(p *Person) { fmt.Printf("函數里接收到的Person內存地址:%p\n",&p) p.Name = "李四" } func main() { p := Person{"張三"} fmt.Printf("原始Person的內存地址是:%p\n",&p) modify(&p) fmt.Println(p) }
運行打印結果:
原始Person的內存地址是:0xc00004a1c0 函數里接收到的Person內存地址:0xc00007e020 {李四}
看上面的輸出結果,我們發現這次被修改了,那么指針類型的可以被修改,非指針類型的不能被修改,是不是我們
使用 make 創建的 map 就是一個指針類型呢?,看一下 Go 源碼:
源碼地址:https://github.com/golang/go/blob/master/src/runtime/map.go
func makemap64(t *maptype, hint int64, h *hmap) *hmap { // 省略無關代碼 }
我們看到 make 函數返回的是一個 hmap 類型的指針 *hmap,也就是 map == *hmap。現在再來看 func modify(p Person)
其實就是 func modify(p *Persoon) ,所以,這里 Go 通過 make 函數,為我們省去了指針的操作,在這里 map 可以理解為引用
類型但不是傳引用
Slice 類型
slice 類型也是引用類型,它也可以在函數中修改對應的內容:
package main import "fmt" func modify(ages []int) { fmt.Printf("函數里接收到 slice 的內存地址:%p\n",ages) ages[0] = 6 } func main() { ages := []int{10,20,30} fmt.Printf("原始 slice 的內存地址:%p\n",ages) modify(ages) fmt.Println(ages) }
運行打印結果,發現的確是被修改了,而且在這里我們打印 slice 內存地址可以直接通過 %p,不用使用
& 取地址符轉換,我們看一下 fmt.Printf 源碼:
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 } // 省略部分代碼 }
通過上面的源碼發現,對於 map、slice 等被當成指針處理,通過 value.Pointer() 獲取對應值的指針:
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 { // As the doc comment says, the returned pointer is an // underlying code pointer but not necessarily enough to // identify a single function uniquely. All method expressions // created via reflect have the same underlying code pointer, // so their Pointers are equal. The function used here must // match the one used in makeMethodValue. f := methodValueCall return **(**uintptr)(unsafe.Pointer(&f)) } p := v.pointer() // Non-nil func value points at data block. // First word of data block is actual code. if p != nil { p = *(*unsafe.Pointer)(p) } return uintptr(p) case Slice: return (*SliceHeader)(v.ptr).Data } panic(&ValueError{"reflect.Value.Pointer", v.kind()}) }
看上面的代碼,如果類型是 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 是一種結構體 +
元素指針的混合類型
Go 語言中所有的傳參都是值傳遞,都是一個副本、一個拷貝。因為拷貝的內容是非引用類型(int、string、struct等這些),這樣就在函數
中無法修改原內容數據的;有的是引用類型(map、slice等),這樣的就可以修改原內容數據
是否可以修改原內容數據,和傳值、傳引用沒有必然的關系,在 Go 語言中,雖然只有傳值,但是我們也可以修改原內容數據,因為參數
是引用類型,引用類型和傳引用是兩個概念
Go 只有值傳遞