Go語言 引數傳遞究竟是值傳遞還是引用傳遞的問題分析
之前我們談過,在Go語言中的引用型別有:對映(map),陣列切片(slice),通道(channel),方法與函式。起初我一直認為,除了以上說的五種是引用傳遞外,其他的都是值傳遞,也就是Go語言中存在值傳遞與引用傳遞,但事實真的如所想的這樣嗎?
我們知道在記憶體中的任何東西都有自己的記憶體地址,普通值,指標都有自己的記憶體地址
i := 10 ip := &i i的記憶體地址為: 0xc042060080,i的指標的記憶體地址為 0xc042080018
比如 我們建立一個整型變數 i,該變數的值為10,有一個指向整型變數 i 的指標ip,該ip包含了 i 的記憶體地址 0xc042060080 。但是ip也有自己的記憶體地址 0xc042080018。
那麼在Go語言傳遞引數時,我們可能會有以下兩種假設:
①函式引數傳遞都是值傳遞,也就是傳遞原值的一個副本。無論是對於整型,字串,布林,陣列等非引用型別,還是對映(map),陣列切片(slice),通道(channel),方法與函式等引用型別,前者是傳遞該值的副本的記憶體地址,後者是傳遞該值的指標的副本的記憶體地址。
②函式傳遞時,既包含整型,字串,布林,陣列等非引用型別的值傳遞,傳遞該值的副本,也包括對映(map),陣列切片(slice),通道(channel),方法與函式等引用型別的引用傳遞,傳遞該值的指標。
現在我們根據上述兩種假設來探討一下。
首先我們知道對於非引用型別:整型,字串,布林,陣列在當作引數傳遞時,是傳遞副本的記憶體地址,也就是值傳遞
func main() { i := 10 //整形變數 i ip := &i //指向整型變數 i 的指標ip,包含了 i 的記憶體地址 fmt.Printf("main中i的值為:%v,i 的記憶體地址為:%v,i的指標的記憶體地址為:%v\n",i,ip,&ip) modifyBypointer(i) fmt.Printf("main中i的值為:%v,i 的記憶體地址為:%v,i的指標的記憶體地址為:%v\n",i,ip,&ip) } func modify(i int) { fmt.Printf("modify i 為:%v,i的指標的記憶體地址為:%v\n",i,&i) i = 11 } ----output---- main中 i 的值為:10,i 的記憶體地址為:0xc0420080b8,i 的指標的記憶體地址為:0xc042004028 modify i 為:10,i 的指標的記憶體地址為:0xc0420080d8 main中 i 的值為:10,i 的記憶體地址為:0xc0420080b8,i 的指標的記憶體地址為:0xc042004028
上面在函式接收的引數中沒有使用指標,所以在傳遞引數時,傳遞的是該值的副本,記憶體地址會改變,因此在函式中對該變數進行操作不會影響到原變數的值。
記憶體分佈圖如下:
非引用型別傳遞記憶體分析 .png如果我將上面函式的引數傳遞方式改一下,改為接收引數的指標
func main() { i := 10 //整形變數 i ip := &i //指向整型變數 i 的指標ip,包含了 i 的記憶體地址 fmt.Printf("main中i的值為:%v,i 的記憶體地址為:%v,i的指標的記憶體地址為:%v\n",i,ip,&ip) modifyBypointer(ip) fmt.Printf("main中i的值為:%v,i 的記憶體地址為:%v,i的指標的記憶體地址為:%v\n",i,ip,&ip) } func modifyBypointer(i *int) { fmt.Printf("modifyBypointer i 的記憶體地址為:%v,i的指標的記憶體地址為:%v\n",i,&i) *i = 11 } ---output--- main中i的值為:10,i 的記憶體地址為:0xc042060080,i的指標ip的記憶體地址為:0xc042080018 modifyBypointer i 的記憶體地址為:0xc042060080,i的指標ip的記憶體地址為:0xc042080028 main中i的值為:11,i 的記憶體地址為:0xc042060080,i的指標ip的記憶體地址為:0xc042080018
將函式的引數改為傳遞指標後,函式內部對變數的修改就會影響到原變數的值,且不會影響到原變數的記憶體地址。但是可以看出main中各個引數的記憶體地址與函式中接收到的記憶體地址不一致,也就是說指標作為函式引數的傳遞過程中,是傳遞了該指標的副本地址,不是原指標地址。
那麼既然函式中的指標地址與main中的指標地址不一致,那麼我們在函式中對變數進行修改時,函式中對變數的修改又怎麼會影響到main中原變數的值呢?
這是因為,雖然函式中的指標地址與main中的指標地址不一致,但是它們都指向同一個整形變數的記憶體地址,所以無論哪一方對變數i進行操作都會影響到變數i,且另一方是可以觀察到的。
我們來看一下這個記憶體分佈圖
引用型別傳遞記憶體分析.png到目前為止,我們驗證了非引用型別和指標的引數傳遞都是傳遞副本,那麼對於引用型別的引數傳遞又是如何的呢?
①對映map
我們使用make初始化一個對映map時,實際上返回的是該對映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 {}
也就是說,對於引用型別map來講,實際上在作為傳遞引數時還是使用了指標的副本進行傳遞,屬於值傳遞。
②chan型別
使用make初始化 chan型別,底層其實跟map一樣,都是返回該值的指標
func makechan(t *chantype, size int) *hchan {}
③Slice型別
Slice型別對於之前的map,chan型別不太一樣,比如下面這個程式碼示例
func main() { i := []int{1,2,3} fmt.Printf("i:%p\n",i) fmt.Println("i[0]:",&i[0]) fmt.Printf("i:%v\n",&i) } ---output--- i:0xc04205e0c0 i[0]: 0xc04205e0c0 i:&[1 2 3]
我們可以看到,使用&操作符表示slice的地址是無效的,而且使用%p輸出的記憶體地址與slice的第一個元素的地址是一樣的,那麼為什麼會出現這樣的情況呢?
我們來看一下在 fmt/print.go中的printValue函式原始碼
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) //然後遞迴獲取vaule的內容 return } }
如果是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("i:%v\n",&i) 會輸出 i:&[1 2 3]的原因。
然後我們再來分析一下為什麼使用%p輸出的記憶體地址與slice的第一個元素的地址是一樣的。
繼續看fmt/print.go中的 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 }
通過原始碼發現,對於chan、map、slice,Func等被當成指標處理,通過value.Pointer獲取對應的值的指標。
value.Pointer的原始碼如下:
// 如果v的型別是Func,則返回的指標是底層程式碼指標,但不一定足以唯一地標識單個函式。 // 唯一的保證是當且僅當v是nil func值時結果為零。 // //如果v的型別是Slice,則返回的指標指向切片的第一個元素。 //如果切片為nil,則返回值為0。如果切片為空但非nil,則返回值為非零。 func (v Value) Pointer() uintptr { k := v.kind() switch k { case Chan, Map, Ptr, UnsafePointer: return uintptr(v.pointer()) case Func: if v.flag&flagMethod != 0 { 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型別的時候,fmt.Printf返回是slice這個結構體裡第一個元素的地址。說到底,又轉變成了指標處理,只不過這個指標是slice中第一個元素的記憶體地址。之前說Slice型別對於之前的map,chan型別不太一樣,不一樣就在於slice是一種結構體+第一個元素指標的混合型別,通過元素array(Data)的指標,可以達到修改slice裡儲存元素的目的。
根據slice與map,chan對比,我們可以總結一條規律:
可以通過某個變數型別本身的指標(如map,chan)或者該變數型別內部的元素的指標(如slice的第一個元素的指標)修改該變數型別的值。
因此slice也跟chan與map一樣,屬於值傳遞,傳遞的是第一個元素的指標的副本。
總結:在Go語言中只存在值傳遞(要麼是該值的副本,要麼是指標的副本),不存在引用傳遞。之所以對於引用型別的傳遞可以修改原內容資料,是因為在底層預設使用該引用型別的指標進行傳遞,但是也是使用指標的副本,依舊是值傳遞。
思考問題:
①既然slice是使用第一個元素的記憶體地址作為slice的指標,那麼如果出現兩個相同的slice,它們的指標豈不會相同
②slice在作為引數傳遞時,可以修改原slice的資料,那麼可以修改原slice的len和cap嗎
參考文章
Go語言引數傳遞是傳值還是傳引用
go中fmt.Println(&array)列印的是陣列地址嗎
轉載:https://www.jianshu.com/p/f201d6da488a