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
型別的引數,傳遞的其實是這個引數的一個副本;傳遞一個指標型別的引數,其實傳遞的是這個該指標的一份拷貝,而不是這個指標指向的值。我們畫個圖來解釋一下:
什麼是引用傳遞
學習過其他語言的同學,對這個引用傳遞應該很熟悉,比如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
也是值傳遞嗎?
先看一段程式碼:
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交流群,我們一起學習交流。
結尾給大家發一個小福利吧,最近我在看[微服務架構設計模式]這一本書,講的很好,自己也收集了一本PDF,有需要的小夥可以到自行下載。獲取方式:關注公眾號:[Golang夢工廠],後臺回覆:[微服務],即可獲取。
我翻譯了一份GIN中文文件,會定期進行維護,有需要的小夥伴後臺回覆[gin]即可下載。
翻譯了一份Machinery中文文件,會定期進行維護,有需要的小夥伴們後臺回覆[machinery]即可獲取。
我是asong,一名普普通通的程式猿,讓gi我一起慢慢變強吧。我自己建了一個golang
交流群,有需要的小夥伴加我vx
,我拉你入群。歡迎各位的關注,我們下期見~~~
推薦往期文章:
相關文章
- Go語言引數傳遞是傳值?還是傳引用 ?Go
- Go語言引數傳遞是傳值還是傳引用Go
- Java引數傳遞是傳值還是傳引用?Java
- 面試官問:Go 中的引數傳遞是值傳遞還是引用傳遞?面試Go
- Java - 是值傳遞還是引用傳遞Java
- 面試官:兄弟,說說Java到底是值傳遞還是引用傳遞面試Java
- 解惑4:java是值傳遞還是引用傳遞Java
- JS是按值傳遞還是按引用傳遞?JS
- Python 函式中,引數是傳值,還是傳引用?Python函式
- js中函式引數值傳遞和引用傳遞JS函式
- GO中的函式設計時候,引數傳遞選擇傳遞值還是傳遞指標?Go函式指標
- GO語言————6.3 傳遞變長引數Go
- Python的函式引數傳遞:傳值?引用?Python函式
- 引數傳遞方式必須是const引用傳遞
- Java是值傳遞還是引用傳遞,又是怎麼體現的Java
- Java 是傳值還是傳引用 (轉)Java
- Python引數傳遞,既不是傳值也不是傳引用Python
- 值傳遞與引用傳遞
- 值傳遞和引用傳遞
- js函式引數值傳遞和引用傳遞簡單介紹JS函式
- 偽命題:Java傳遞的值還是引用?Java
- JavaScript的值傳遞和引用傳遞JavaScript
- 快速搞懂值傳遞與引用傳遞
- Java的值傳遞和引用傳遞Java
- 188W+程式設計師關注過的問題:Java到底是值傳遞還是引用傳遞?程式設計師Java
- 關於String是值傳遞還是引用傳遞,talk is cheap, just show codes
- 搗漿糊的老話題:Java是按值傳遞還是按引用傳遞Java
- JS的方法引數傳遞(按值傳遞)JS
- Java中的值傳遞和引用傳遞Java
- Day30--值傳遞和引用傳遞
- 你會swap嗎,按值傳遞還是按引用?
- ABAP 方法呼叫的引數傳遞裡,透過引用傳遞的方式,能修改原始引數值嗎?
- java經典問題:傳值還是傳引用(轉)Java
- go 值傳遞和地址傳遞的例子Go
- golang工作筆記(二)值傳遞與引用傳遞Golang筆記
- 關於值傳遞和引用傳遞的解釋
- java方法中只有值傳遞,沒有引用傳遞Java
- C#引數傳遞之值引數C#