本文爲原創文章,轉載註明出處,歡迎掃碼關注公衆號
flysnow_org
或者網站www.flysnow.org/,第一時間看後續精彩文章。以爲好的話,順手分享到朋友圈吧,感謝支持。數組
對於瞭解一門語言來講,會關心咱們在函數調用的時候,參數究竟是傳的值,仍是引用?bash
其實對於傳值和傳引用,是一個比較古老的話題,作研發的都有這個概念,可是可能不是很是清楚。對於咱們作Go語言開發的來講,也想知道究竟是什麼傳遞。函數
那麼咱們先來看看什麼是值傳遞,什麼是引用傳遞。網站
傳值的意思是:函數傳遞的老是原來這個東西的一個副本,一副拷貝。好比咱們傳遞一個int
類型的參數,傳遞的實際上是這個參數的一個副本;傳遞一個指針類型的參數,其實傳遞的是這個該指針的一份拷貝,而不是這個指針指向的值。ui
對於int這類基礎類型咱們能夠很好的理解,它們就是一個拷貝,可是指針呢?咱們以爲能夠經過它修改原來的值,怎麼會是一個拷貝呢?下面咱們看個例子。spa
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
複製代碼
首先咱們要知道,任何存放在內存裏的東西都有本身的地址,指針也不例外,它雖然指向別的數據,可是也有存放該指針的內存。code
因此經過輸出咱們能夠看到,這是一個指針的拷貝,由於存放這兩個指針的內存地址是不一樣的,雖然指針的值相同,可是是兩個不一樣的指針。cdn
經過上面的圖,能夠更好的理解。 首先咱們看到,咱們聲明瞭一個變量i
,值爲10
,它的內存存放地址是0xc420018070
,經過這個內存地址,咱們能夠找到變量i
,這個內存地址也就是變量i
的指針ip
。blog
指針ip
也是一個指針類型的變量,它也須要內存存放它,它的內存地址是多少呢?是0xc42000c028
。 在咱們傳遞指針變量ip
給modify
函數的時候,是該指針變量的拷貝,因此新拷貝的指針變量ip
,它的內存地址已經變了,是新的0xc42000c038
。
不論是0xc42000c028
仍是0xc42000c038
,咱們均可以稱之爲指針的指針,他們指向同一個指針0xc420018070
,這個0xc420018070
又指向變量i
,這也就是爲何咱們能夠修改變量i
的值。
Go語言(Golang)是沒有引用傳遞的,這裏我不能使用Go舉例子,可是能夠經過說明描述。
以上面的例子爲例,若是在modify
函數裏打印出來的內存地址是不變的,也是0xc42000c028
,那麼就是引用傳遞。
瞭解清楚了傳值和傳引用,可是對於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
類型本質上和map
類型是同樣的,這裏不作過多的介紹,參考下源代碼:
func makechan(t *chantype, size int64) *hchan {
//省略無關代碼
}
複製代碼
chan
也是一個引用類型,和map
相差無幾,make
返回的是一個*hchan
。
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/,第一時間看後續精彩文章。以爲好的話,順手分享到朋友圈吧,感謝支持。