Go語言參數傳遞是傳值還是傳引用
對於瞭解一門語言來說,會關心我們在函數調用的時候,參數到底是傳的值,還是引用?
其實對於傳值和傳引用,是一個比較古老的話題,做研發的都有這個概念,但是可能不是非常清楚。對於我們做Go語言開發的來說,也想知道到底是什麼傳遞。
那麼我們先來看看什麼是值傳遞,什麼是引用傳遞。
什麼是傳值(值傳遞)
傳值的意思是:函數傳遞的總是原來這個東西的一個副本,一副拷貝。比如我們傳遞一個int類型的參數,傳遞的其實是這個參數的一個副本;傳遞一個指針類型的參數,其實傳遞的是這個該指針的一份拷貝,而不是這個指針指向的值。
對於int這類基礎類型我們可以很好的理解,它們就是一個拷貝,但是指針呢?我們覺得可以通過它修改原來的值,怎麼會是一個拷貝呢?下面我們看個例子。
func main() { i:=10 ip:=&i fmt.Printf("原始指針的內存地址是:%p\n",&ip) modify(ip) fmt.Println("int值被修改瞭,新值為:",i) } func modify(ip *int){ fmt.Printf("函數裡接收到的指針的內存地址是:%p\n",&ip) *ip=1 }
我們運行,可以看到輸入結果如下:
原始指針的內存地址是:0xc42000c028
函數裡接收到的指針的內存地址是:0xc42000c038
int值被修改瞭,新值為: 1
首先我們要知道,任何存放在內存裡的東西都有自己的地址,指針也不例外,它雖然指向別的數據,但是也有存放該指針的內存。
所以通過輸出我們可以看到,這是一個指針的拷貝,因為存放這兩個指針的內存地址是不同的,雖然指針的值相同,但是是兩個不同的指針。
通過上面的圖,可以更好的理解。 首先我們看到,我們聲明瞭一個變量i,值為10,它的內存存放地址是0xc420018070,通過這個內存地址,我們可以找到變量i,這個內存地址也就是變量i的指針ip。
指針ip也是一個指針類型的變量,它也需要內存存放它,它的內存地址是多少呢?是0xc42000c028。 在我們傳遞指針變量ip給modify函數的時候,是該指針變量的拷貝,所以新拷貝的指針變量ip,它的內存地址已經變瞭,是新的0xc42000c038。
不管是0xc42000c028還是0xc42000c038,我們都可以稱之為指針的指針,他們指向同一個指針0xc420018070,這個0xc420018070又指向變量i,這也就是為什麼我們可以修改變量i的值。
什麼是傳引用(引用傳遞)
Go語言(Golang)是沒有引用傳遞的,這裡我不能使用Go舉例子,但是可以通過說明描述。
以上面的例子為例,如果在modify函數裡打印出來的內存地址是不變的,也是0xc42000c028,那麼就是引用傳遞。
迷惑Map
瞭解清楚瞭傳值和傳引用,但是對於Map類型來說,可能覺得還是迷惑,一來我們可以通過方法修改它的內容,二來它沒有明顯的指針。
func main() { persons:=make(map[string]int) persons["張三"]=19 mp:=&persons fmt.Printf("原始map的內存地址是:%p\n",mp) modify(persons) fmt.Println("map值被修改瞭,新值為:",persons) } func modify(p map[string]int){ fmt.Printf("函數裡接收到map的內存地址是:%p\n",&p) p["張三"]=20 }
運行打印輸出:
原始map的內存地址是:0xc42000c028
函數裡接收到map的內存地址是:0xc42000c038
map值被修改瞭,新值為: map[張三:20]
兩個內存地址是不一樣的,所以這又是一個值傳遞(值的拷貝),那麼為什麼我們可以修改Map的內容呢?先不急,我們先看一個自己實現的struct。
func main() { p:=Person{"張三"} fmt.Printf("原始Person的內存地址是:%p\n",&p) modify(p) fmt.Println(p) } type Person struct { Name string } func modify(p Person) { fmt.Printf("函數裡接收到Person的內存地址是:%p\n",&p) p.Name = "李四" }
運行打印輸出:
原始Person的內存地址是:0xc4200721b0
函數裡接收到Person的內存地址是:0xc4200721c0
{張三}
我們發現,我們自己定義的Person類型,在函數傳參的時候也是值傳遞,但是它的值(Name字段)並沒有被修改,我們想改成李四,發現最後的結果還是張三。
這也就是說,map類型和我們自己定義的struct類型是不一樣的。我們嘗試把modify函數的接收參數改為Person的指針。
func main() { p:=Person{"張三"} modify(&p) fmt.Println(p) } type Person struct { Name string } func modify(p *Person) { p.Name = "李四" }
在運行查看輸出,我們發現,這次被修改瞭。我們這裡省略瞭內存地址的打印,因為我們上面int類型的例子已經證明瞭指針類型的參數也是值傳遞的。 指針類型可以修改,非指針類型不行,那麼我們可以大膽的猜測,我們使用make函數創建的map是不是一個指針類型呢?看一下源代碼:
// makemap implements a Go map creation 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 bucket != nil, bucket can be used as the first bucket. func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap { //省略無關代碼 }
通過查看src/runtime/hashmap.go源代碼發現,的確和我們猜測的一樣,make函數返回的是一個hmap類型的指針*hmap。也就是說map===*hmap。 現在看func modify(p map)這樣的函數,其實就等於func modify(p *hmap),和我們前面第一節什麼是值傳遞裡舉的func modify(ip *int)的例子一樣,可以參考分析。
所以在這裡,Go語言通過make函數,字面量的包裝,為我們省去瞭指針的操作,讓我們可以更容易的使用map。這裡的map可以理解為引用類型,但是記住引用類型不是傳引用。
chan類型
chan類型本質上和map類型是一樣的,這裡不做過多的介紹,參考下源代碼: func makechan(t *chantype, size int64) *hchan { //省略無關代碼 }
chan也是一個引用類型,和map相差無幾,make返回的是一個*hchan。
和map、chan都不一樣的slice
slice和map、chan都不太一樣的,一樣的是,它也是引用類型,它也可以在函數中修改對應的內容。
func main() { ages:=[]int{6,6,6} fmt.Printf("原始slice的內存地址是%p\n",ages) modify(ages) fmt.Println(ages) } func modify(ages []int){ fmt.Printf("函數裡接收到slice的內存地址是%p\n",ages) ages[0]=1 }
運行打印結果,發現的確是被修改瞭,而且我們這裡打印slice的內存地址是可以直接通過%p打印的,不用使用&取地址符轉換。
這就可以證明make的slice也是一個指針瞭嗎?不一定,也可能fmt.Printf把slice特殊處理瞭。
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等被當成指針處理,通過value.Pointer()獲取對應的值的指針。
// 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 Slice: return (*SliceHeader)(v.ptr).Data } }
很明顯瞭,當是slice類型的時候,返回是slice這個結構體裡,字段Data第一個元素的地址。
type SliceHeader struct { Data uintptr Len int Cap int } type slice struct { array unsafe.Pointer len int cap int }
所以我們通過%p打印的slice變量ages的地址其實就是內部存儲數組元素的地址,slice是一種結構體+元素指針的混合類型,通過元素array(Data)的指針,可以達到修改slice裡存儲元素的目的。
所以修改類型的內容的辦法有很多種,類型本身作為指針可以,類型裡有指針類型的字段也可以。
單純的從slice這個結構體看,我們可以通過modify修改存儲元素的內容,但是永遠修改不瞭len和cap,因為他們隻是一個拷貝,如果要修改,那就要傳遞*slice作為參數才可以。
func main() { i:=19 p:=Person{name:"張三",age:&i} fmt.Println(p) modify(p) fmt.Println(p) } type Person struct { name string age *int } func (p Person) String() string{ return "姓名為:" + p.name + ",年齡為:"+ strconv.Itoa(*p.age) } func modify(p Person){ p.name = "李四" *p.age = 20 }
運行打印輸出結果為:
姓名為:張三,年齡為:19
姓名為:張三,年齡為:20
通過這個Person和slice對比,就更好理解瞭,Person的name字段就類似於slice的len和cap字段,age字段類似於array字段。在傳參為非指針類型的情況下,隻能修改age字段,name字段無法修改。要修改name字段,就要把傳參改為指針,比如:
modify(&p) func modify(p *Person){ p.name = "李四" *p.age = 20 }
這樣name和age字段雙雙都被修改瞭。
所以slice類型也是引用類型。
小結
最終我們可以確認的是Go語言中所有的傳參都是值傳遞(傳值),都是一個副本,一個拷貝。因為拷貝的內容有時候是非引用類型(int、string、struct等這些),這樣就在函數中就無法修改原內容數據;有的是引用類型(指針、map、slice、chan等這些),這樣就可以修改原內容數據。
是否可以修改原內容數據,和傳值、傳引用沒有必然的關系。在C++中,傳引用肯定是可以修改原內容數據的,在Go語言裡,雖然隻有傳值,但是我們也可以修改原內容數據,因為參數是引用類型。
這裡也要記住,引用類型和傳引用是兩個概念。
再記住,Go裡隻有傳值(值傳遞)。
到此這篇關於Go語言參數傳遞是傳值還是傳引用的文章就介紹到這瞭,更多相關Go語言參數傳遞內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 關於Go 是傳值還是傳引用?
- Go Slice擴容的這些坑你踩過哪些
- Go利用反射reflect實現獲取接口變量信息
- Go map發生內存泄漏解決方法
- Go結構體SliceHeader及StringHeader作用詳解