天天看點

go語言參數傳遞到底是傳值還是傳引用

前言

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

實參與形參數

我們使用​

​go​

​定義方法時是可以定義參數的。比如如下方法:

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​

​類型的參數,傳遞的其實是這個參數的一個副本;傳遞一個指針類型的參數,其實傳遞的是這個該指針的一份拷貝,而不是這個指針指向的值。我們畫個圖來解釋一下:

go語言參數傳遞到底是傳值還是傳引用

什麼是引用傳遞

學習過其他語言的同學,對這個引用傳遞應該很熟悉,比如​

​C++​

​使用者,在C++中,函數參數的傳遞方式有引用傳遞。所謂引用傳遞是指在調用函數時将實際參數的位址傳遞到函數中,那麼在函數中對參數所進行的修改,将影響到實際參數。

go語言參數傳遞到底是傳值還是傳引用

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      

是以通過輸出我們可以看到,這是一個指針的拷貝,因為存放這兩個指針的記憶體位址是不同的,雖然指針的值相同,但是是兩個不同的指針。

go語言參數傳遞到底是傳值還是傳引用

通過上面的圖,我們可以更好的了解。我們聲明了一個變量​

​args​

​​,其值為​

​1​

​​,并且他的記憶體存放位址是​

​0xc0000b4008​

​​,通過這個位址,我們就可以找到變量​

​args​

​​,這個位址也就是變量​

​args​

​​的指針​

​addr​

​​。指針​

​addr​

​​也是一個指針類型的變量,它也需要記憶體存放它,它的記憶體位址是多少呢?是​

​0xc0000ae018​

​​。 在我們傳遞指針變量​

​addr​

​​給​

​modifiedNumber​

​​函數的時候,是該指針變量的拷貝,是以新拷貝的指針變量​

​addr​

​​,它的記憶體位址已經變了,是新的​

​0xc0000ae028​

​​。是以,不管是​

​0xc0000ae018​

​​還是​

​0xc0000ae028​

​​,我們都可以稱之為指針的指針,他們指向同一個指針​

​0xc0000b4008​

​​,這個​

​0xc0000b4008​

​​又指向變量​

​args​

​​,這也就是為什麼我們可以修改變量​

​args​

​的值。

通過上面的分析,我們就可以确定​

​go​

​​就是值傳遞,因為我們在​

​modifieNumber​

​​方法中列印出來的記憶體位址發生了改變,是以不是引用傳遞,實錘了奧兄弟們,證據确鑿~~~。等等,好像好落下了點什麼,說好的go中隻有值傳遞呢,為什麼​

​chan​

​​、​

​map​

​​、​

​slice​

​類型傳遞卻可以改變其中的值呢?白着急,我們依次來驗證一下。

​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也是值傳遞嗎?

​map​

​​和​

​slice​

​​一樣都具有迷惑行為,哼,渣女。​

​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交流群,我們一起學習交流。