本文為原創文章,轉載註明出處,歡迎掃碼關注公眾號
flysnow_org
或者網站www.flysnow.org/,第一時間看後續精彩文章。覺得好的話,順手分享到朋友圈吧,感謝支援。
對於瞭解一門語言來說,會關心我們在函式呼叫的時候,引數到底是傳的值,還是引用?
其實對於傳值和傳引用,是一個比較古老的話題,做研發的都有這個概念,但是可能不是非常清楚。對於我們做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裡只有傳值(值傳遞)。
本文為原創文章,轉載註明出處,歡迎掃碼關注公眾號
flysnow_org
或者網站www.flysnow.org/,第一時間看後續精彩文章。覺得好的話,順手分享到朋友圈吧,感謝支援。