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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM