go語言參數傳遞究竟是傳值仍是傳引用?

前言

哈嘍,你們好,我是asong。今天女友問我,小松子,你知道Go語言參數傳遞是傳值仍是傳引用嗎?哎呀哈,我居然被瞧不起了,我立馬一頓操做,給他講的明明白白的,小丫頭片子,仍是太嫩,你們且聽我細細道來~~~。​

實參與形參數

咱們使用go定義方法時是能夠定義參數的。好比以下方法:golang

func printNumber(args ...int)

這裏的args就是參數。參數在程序語言中分爲形式參數和實際參數。面試

形式參數:是在定義函數名和函數體的時候使用的參數,目的是用來接收調用該函數時傳入的參數。設計模式

實際參數:在調用有參函數時,主調函數和被調函數之間有數據傳遞關係。在主調函數中調用一個函數時,函數名後面括號中的參數稱爲「實際參數」。數組

舉例以下:緩存

func main()  {
 var args int64= 1
 printNumber(args)  // args就是實際參數
}

func printNumber(args ...int64)  { //這裏定義的args就是形式參數
    for _,arg := range args{
        fmt.Println(arg) 
    }
}

什麼是值傳遞

值傳遞,咱們分析其字面意思:傳遞的就是值。傳值的意思是:函數傳遞的老是原來這個東西的一個副本,一副拷貝。好比咱們傳遞一個int類型的參數,傳遞的實際上是這個參數的一個副本;傳遞一個指針類型的參數,其實傳遞的是這個該指針的一份拷貝,而不是這個指針指向的值。咱們畫個圖來解釋一下:架構

什麼是引用傳遞

學習過其餘語言的同窗,對這個引用傳遞應該很熟悉,好比C++使用者,在C++中,函數參數的傳遞方式有引用傳遞。所謂引用傳遞是指在調用函數時將實際參數的地址傳遞到函數中,那麼在函數中對參數所進行的修改,將影響到實際參數。app

golang是值傳遞

咱們先寫一個簡單的例子驗證一下:異步

func main()  {
 var args int64= 1
 modifiedNumber(args) // args就是實際參數
 fmt.Printf("實際參數的地址 %p\n", &args)
 fmt.Printf("改動後的值是  %d\n",args)
}

func modifiedNumber(args int64)  { //這裏定義的args就是形式參數
    fmt.Printf("形參地址 %p \n",&args)
    args = 10
}

運行結果:函數

形參地址 0xc0000b4010 
實際參數的地址 0xc0000b4008
改動後的值是  1

這裏正好驗證了go是值傳遞,可是還不能徹底肯定go就只有值傳遞,咱們在寫一個例子驗證一下:微服務

func main()  {
 var args int64= 1
 addr := &args
 fmt.Printf("原始指針的內存地址是 %p\n", addr)
 fmt.Printf("指針變量addr存放的地址 %p\n", &addr)
 modifiedNumber(addr) // args就是實際參數
 fmt.Printf("改動後的值是  %d\n",args)
}

func modifiedNumber(addr *int64)  { //這裏定義的args就是形式參數
    fmt.Printf("形參地址 %p \n",&addr)
    *addr = 10
}

運行結果:

原始指針的內存地址是 0xc0000b4008
指針變量addr存放的地址 0xc0000ae018
形參地址 0xc0000ae028 
改動後的值是  10

因此經過輸出咱們能夠看到,這是一個指針的拷貝,由於存放這兩個指針的內存地址是不一樣的,雖然指針的值相同,可是是兩個不一樣的指針。

經過上面的圖,咱們能夠更好的理解。咱們聲明瞭一個變量args,其值爲1,而且他的內存存放地址是0xc0000b4008,經過這個地址,咱們就能夠找到變量args,這個地址也就是變量args的指針addr。指針addr也是一個指針類型的變量,它也須要內存存放它,它的內存地址是多少呢?是0xc0000ae018。 在咱們傳遞指針變量addrmodifiedNumber函數的時候,是該指針變量的拷貝,因此新拷貝的指針變量addr,它的內存地址已經變了,是新的0xc0000ae028。因此,不論是0xc0000ae018仍是0xc0000ae028,咱們均可以稱之爲指針的指針,他們指向同一個指針0xc0000b4008,這個0xc0000b4008又指向變量args,這也就是爲何咱們能夠修改變量args的值。

經過上面的分析,咱們就能夠肯定go就是值傳遞,由於咱們在modifieNumber方法中打印出來的內存地址發生了改變,因此不是引用傳遞,實錘了奧兄弟們,證據確鑿~~~。等等,好像好落下了點什麼,說好的go中只有值傳遞呢,爲何chanmapslice類型傳遞卻能夠改變其中的值呢?白着急,咱們依次來驗證一下。

slice也是值傳遞嗎?

先看一段代碼:

func main()  {
 var args =  []int64{1,2,3}
 fmt.Printf("切片args的地址: %p\n",args)
 modifiedNumber(args)
 fmt.Println(args)
}

func modifiedNumber(args []int64)  {
    fmt.Printf("形參切片的地址 %p \n",args)
    args[0] = 10
}

運行結果:

切片args的地址: 0xc0000b8000
形參切片的地址 0xc0000b8000 
[10 2 3]

哇去,怎麼回事,光速打臉呢,這怎麼地址都是同樣的呢?而且值還被修改了呢?怎麼回事,做何解釋,你個渣男,欺騙我感情。。。很差意思走錯片場了。繼續來看這個問題。這裏咱們沒有使用&符號取地址符轉換,就把slice地址打印出來了,咱們在加上一行代碼測試一下:

func main()  {
 var args =  []int64{1,2,3}
 fmt.Printf("切片args的地址: %p \n",args)
 fmt.Printf("切片args第一個元素的地址: %p \n",&args[0])
 fmt.Printf("直接對切片args取地址%v \n",&args)
 modifiedNumber(args)
 fmt.Println(args)
}

func modifiedNumber(args []int64)  {
    fmt.Printf("形參切片的地址 %p \n",args)
    fmt.Printf("形參切片args第一個元素的地址: %p \n",&args[0])
    fmt.Printf("直接對形參切片args取地址%v \n",&args)
    args[0] = 10
}

運行結果:

切片args的地址: 0xc000016140 
切片args第一個元素的地址: 0xc000016140 
直接對切片args取地址&[1 2 3] 
形參切片的地址 0xc000016140 
形參切片args第一個元素的地址: 0xc000016140 
直接對形參切片args取地址&[1 2 3] 
[10 2 3]

經過這個例子咱們能夠看到,使用&操做符表示slice的地址是無效的,並且使用%p輸出的內存地址與slice的第一個元素的地址是同樣的,那麼爲何會出現這樣的狀況呢?會不會是fmt.Printf函數作了什麼特殊處理?咱們來看一下其源碼:

fmt包,print.go中的printValue這個方法,截取重點部分,由於`slice`也是引用類型,因此會進入這個`case`:
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)
                return
            }
        }
        fallthrough
    case reflect.Chan, reflect.Func, reflect.UnsafePointer:
        p.fmtPointer(f, verb)

p.buf.writeByte('&')這行代碼就是爲何咱們使用&打印地址輸出結果前面帶有&的語音。由於咱們要打印的是一個slice類型,就會調用p.printValue(a, verb, depth+1)遞歸獲取切片中的內容,爲何打印出來的切片中還會有[]包圍呢,我來看一下printValue這個方法的源代碼:

case reflect.Array, reflect.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("直接對切片args取地址%v \\n",&args)輸出直接對切片args取地址&[1 2 3] 的緣由。這個問題解決了,咱們再來看一看使用%p輸出的內存地址與slice的第一個元素的地址是同樣的。在上面的源碼中,有這樣一行代碼fallthrough,表明着接下來的fmt.Poniter也會被執行,我看一下其源碼:

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
    }
...... 省略部分代碼
// If v's Kind is Slice, the returned pointer is to the first
// element of the slice. If the slice is nil the returned value
// is 0.  If the slice is empty but non-nil the return value is non-zero.
 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 {
 ....... 省略部分代碼

這裏咱們能夠看到上面有這樣一句註釋:If v's Kind is Slice, the returned pointer is to the first。翻譯成中文就是若是是slice類型,返回slice這個結構裏的第一個元素的地址。這裏正好解釋上面爲何fmt.Printf("切片args的地址: %p \\n",args)fmt.Printf("形參切片的地址 %p \\n",args)打印出來的地址是同樣的,由於args是引用類型,因此他們都返回slice這個結構裏的第一個元素的地址,爲何這兩個slice結構裏的第一個元素的地址同樣呢,這就要在說一說slice的底層結構了。

咱們看一下slice底層結構:

//runtime/slice.go
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

slice是一個結構體,他的第一個元素是一個指針類型,這個指針指向的是底層數組的第一個元素。因此當是slice類型的時候,fmt.Printf返回是slice這個結構體裏第一個元素的地址。說到底,又轉變成了指針處理,只不過這個指針是slice中第一個元素的內存地址。

說了這麼多,最後再作一個總結吧,爲何slice也是值傳遞。之因此對於引用類型的傳遞能夠修改原內容的數據,這是由於在底層默認使用該引用類型的指針進行傳遞,但也是使用指針的副本,依舊是值傳遞。因此slice傳遞的就是第一個元素的指針的副本,由於fmt.printf緣故形成了打印的地址同樣,給人一種混淆的感受。

map也是值傳遞嗎?

mapslice同樣都具備迷惑行爲,哼,渣女。map咱們能夠經過方法修改它的內容,而且它沒有明顯的指針。好比這個例子:

func main()  {
    persons:=make(map[string]int)
    persons["asong"]=8

    addr:=&persons

    fmt.Printf("原始map的內存地址是:%p\n",addr)
    modifiedAge(persons)
    fmt.Println("map值被修改了,新值爲:",persons)
}

func modifiedAge(person map[string]int)  {
    fmt.Printf("函數裏接收到map的內存地址是:%p\n",&person)
    person["asong"]=9
}

看一眼運行結果:

原始map的內存地址是:0xc00000e028
函數裏接收到map的內存地址是:0xc00000e038
map值被修改了,新值爲: map[asong:9]

先喵一眼,哎呀,實參與形參地址不同,應該是值傳遞無疑了,等等。。。。map值怎麼被修改了?一臉疑惑。。。。。

爲了解決咱們的疑惑,咱們從源碼入手,看一看什麼原理:

//src/runtime/map.go
// 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 {
    mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
    if overflow || mem > maxAlloc {
        hint = 0
    }

    // initialize Hmap
    if h == nil {
        h = new(hmap)
    }
    h.hash0 = fastrand()

從以上源碼,咱們能夠看出,使用make函數返回的是一個hmap類型的指針*hmap。回到上面那個例子,咱們的func modifiedAge(person map[string]int)函數,其實就等於func modifiedAge(person *hmap),實際上在做爲傳遞參數時仍是使用了指針的副本進行傳遞,屬於值傳遞。在這裏,Go語言經過make函數,字面量的包裝,爲咱們省去了指針的操做,讓咱們能夠更容易的使用map。這裏的map能夠理解爲引用類型,可是記住引用類型不是傳引用。

chan是值傳遞嗎?

老樣子,先看一個例子:

func main()  {
    p:=make(chan bool)
    fmt.Printf("原始chan的內存地址是:%p\n",&p)
    go func(p chan bool){
        fmt.Printf("函數裏接收到chan的內存地址是:%p\n",&p)
        //模擬耗時
        time.Sleep(2*time.Second)
        p<-true
    }(p)

    select {
    case l := <- p:
        fmt.Println(l)
    }
}

再看一看運行結果:

原始chan的內存地址是:0xc00000e028
函數裏接收到chan的內存地址是:0xc00000e038
true

這個怎麼回事,實參與形參地址不同,可是這個值是怎麼傳回來的,說好的值傳遞呢?白着急,鐵子,咱們像分析map那樣,再來分析一下chan。首先看源碼:

// src/runtime/chan.go
func makechan(t *chantype, size int) *hchan {
    elem := t.elem

    // compiler checks this but be safe.
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
    if hchanSize%maxAlign != 0 || elem.align > maxAlign {
        throw("makechan: bad alignment")
    }

    mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
        panic(plainError("makechan: size out of range"))
    }

從以上源碼,咱們能夠看出,使用make函數返回的是一個hchan類型的指針*hchan。這不是與map一個道理嘛,再次回到上面的例子,實際咱們的fun (p chan bool)fun (p *hchan)是同樣的,實際上在做爲傳遞參數時仍是使用了指針的副本進行傳遞,屬於值傳遞。

是否是到這裏,基本就能夠肯定go就是值傳遞了呢?還剩最後一個沒有測試,那就是struct,咱們最後來驗證一下struct

struct就是值傳遞

沒錯,我先說答案,struct就是值傳遞,不信你看這個例子:

func main()  {
    per := Person{
        Name: "asong",
        Age: int64(8),
    }
    fmt.Printf("原始struct地址是:%p\n",&per)
    modifiedAge(per)
    fmt.Println(per)
}

func modifiedAge(per Person)  {
    fmt.Printf("函數裏接收到struct的內存地址是:%p\n",&per)
    per.Age = 10
}

咱們發現,咱們本身定義的Person類型,在函數傳參的時候也是值傳遞,可是它的值(Age字段)並無被修改,咱們想改爲10,發現最後的結果仍是8

前文總結

兄弟們實錘了奧,go就是值傳遞,能夠確認的是Go語言中全部的傳參都是值傳遞(傳值),都是一個副本,一個拷貝。由於拷貝的內容有時候是非引用類型(int、string、struct等這些),這樣就在函數中就沒法修改原內容數據;有的是引用類型(指針、map、slice、chan等這些),這樣就能夠修改原內容數據。

是否能夠修改原內容數據,和傳值、傳引用沒有必然的關係。在C++中,傳引用確定是能夠修改原內容數據的,在Go語言裏,雖然只有傳值,可是咱們也能夠修改原內容數據,由於參數是引用類型。

有的小夥伴會在這裏仍是懵逼,由於你把引用類型和傳引用當成一個概念了,這是兩個概念,切記!!!

出個題考驗大家一下

歡迎在評論區留下你的答案~~~

既然大家都知道了golang只有值傳遞,那麼這段代碼來幫我分析一下吧,這裏的值能修改爲功,爲何使用append不會發生擴容?

func main() {
    array := []int{7,8,9}
    fmt.Printf("main ap brfore: len: %d cap:%d data:%+v\n", len(array), cap(array), array)
    ap(array)
    fmt.Printf("main ap after: len: %d cap:%d data:%+v\n", len(array), cap(array), array)
}

func ap(array []int) {
    fmt.Printf("ap brfore:  len: %d cap:%d data:%+v\n", len(array), cap(array), array)
  array[0] = 1
    array = append(array, 10)
    fmt.Printf("ap after:   len: %d cap:%d data:%+v\n", len(array), cap(array), array)
}

後記

好啦,這一篇文章到這就結束了,咱們下期見~~。但願對大家有用,又不對的地方歡迎指出,可添加個人golang交流羣,咱們一塊兒學習交流。

結尾給你們發一個小福利吧,最近我在看[微服務架構設計模式]這一本書,講的很好,本身也收集了一本PDF,有須要的小夥能夠到自行下載。獲取方式:關注公衆號:[Golang夢工廠],後臺回覆:[微服務],便可獲取。

我翻譯了一份GIN中文文檔,會按期進行維護,有須要的小夥伴後臺回覆[gin]便可下載。

翻譯了一份Machinery中文文檔,會按期進行維護,有須要的小夥伴們後臺回覆[machinery]便可獲取。

我是asong,一名普普統統的程序猿,讓gi我一塊兒慢慢變強吧。我本身建了一個golang交流羣,有須要的小夥伴加我vx,我拉你入羣。歡迎各位的關注,咱們下期見~~~

推薦往期文章:

相關文章
相關標籤/搜索