前言
哈喽,大家好,我是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
類型的參數,傳遞的其實是這個參數的一個副本;傳遞一個指針類型的參數,其實傳遞的是這個該指針的一份拷貝,而不是這個指針指向的值。我們畫個圖來解釋一下:

什麼是引用傳遞
學習過其他語言的同學,對這個引用傳遞應該很熟悉,比如
C++
使用者,在C++中,函數參數的傳遞方式有引用傳遞。所謂引用傳遞是指在調用函數時将實際參數的位址傳遞到函數中,那麼在函數中對參數所進行的修改,将影響到實際參數。
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
。 在我們傳遞指針變量
addr
給
modifiedNumber
函數的時候,是該指針變量的拷貝,是以新拷貝的指針變量
addr
,它的記憶體位址已經變了,是新的
0xc0000ae028
。是以,不管是
0xc0000ae018
還是
0xc0000ae028
,我們都可以稱之為指針的指針,他們指向同一個指針
0xc0000b4008
,這個
0xc0000b4008
又指向變量
args
,這也就是為什麼我們可以修改變量
args
的值。
通過上面的分析,我們就可以确定
go
就是值傳遞,因為我們在
modifieNumber
方法中列印出來的記憶體位址發生了改變,是以不是引用傳遞,實錘了奧兄弟們,證據确鑿~~~。等等,好像好落下了點什麼,說好的go中隻有值傳遞呢,為什麼
chan
、
map
、
slice
類型傳遞卻可以改變其中的值呢?白着急,我們依次來驗證一下。
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
沒錯,我先說答案,
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交流群,我們一起學習交流。