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里存儲元素的目的。

type slice struct {
        array unsafe.Pointer  //這里的指針其實是第一個元素的指針
        len   int
        cap   int
}

根據slice與map,chan對比,我們可以總結一條規律:
可以通過某個變量類型本身的指針(如map,chan)或者該變量類型內部的元素的指針(如slice的第一個元素的指針)修改該變量類型的值。

因此slice也跟chan與map一樣,屬于值傳遞,傳遞的是第一個元素的指針的副本。

總結:在Go語言中只存在值傳遞(要么是該值的副本,要么是指針的副本),不存在引用傳遞。之所以對于引用類型的傳遞可以修改原內容數據,是因為在底層默認使用該引用類型的指針進行傳遞,但是也是使用指針的副本,依舊是值傳遞。

思考問題:
①既然slice是使用第一個元素的內存地址作為slice的指針,那么如果出現兩個相同的slice,它們的指針豈不會相同

②slice在作為參數傳遞時,可以修改原slice的數據,那么可以修改原slice的len和cap嗎

參考文章
Go語言參數傳遞是傳值還是傳引用
go中fmt.Println(&array)打印的是數組地址嗎

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容