1. 程式人生 > 其它 >Go語言 引數傳遞究竟是值傳遞還是引用傳遞的問題分析

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