之前我們談過,在Go語言中的引用類型有:映射(map),數組切片(slice),通道(channel),方法與函數。起初我一直認為,除了以上說的五種是引用傳遞外,其他的都是值傳遞,也就是Go語言中存在值傳遞與引用傳遞,但事實真的如所想的這樣嗎?
我們知道在內存中的任何東西都有自己的內存地址,普通值,指針都有自己的內存地址
i := 10
ip := &i
i的內存地址為: 0xc042060080,i的指針的內存地址為 0xc042080018
比如 我們創建一個整型變量 i,該變量的值為10,有一個指向整型變量 i 的指針ip,該ip包含了 i 的內存地址 0xc042060080 。但是ip也有自己的內存地址 0xc042080018。
那么在Go語言傳遞參數時,我們可能會有以下兩種假設:
①函數參數傳遞都是值傳遞,也就是傳遞原值的一個副本。無論是對于整型,字符串,布爾,數組等非引用類型,還是映射(map),數組切片(slice),通道(channel),方法與函數等引用類型,前者是傳遞該值的副本的內存地址,后者是傳遞該值的指針的副本的內存地址。
②函數傳遞時,既包含整型,字符串,布爾,數組等非引用類型的值傳遞,傳遞該值的副本,也包括映射(map),數組切片(slice),通道(channel),方法與函數等引用類型的引用傳遞,傳遞該值的指針。
現在我們根據上述兩種假設來探討一下。
首先我們知道對于非引用類型:整型,字符串,布爾,數組在當作參數傳遞時,是傳遞副本的內存地址,也就是值傳遞
func main() {
i := 10 //整形變量 i
ip := &i //指向整型變量 i 的指針ip,包含了 i 的內存地址
fmt.Printf("main中i的值為:%v,i 的內存地址為:%v,i的指針的內存地址為:%v\n",i,ip,&ip)
modifyBypointer(i)
fmt.Printf("main中i的值為:%v,i 的內存地址為:%v,i的指針的內存地址為:%v\n",i,ip,&ip)
}
func modify(i int) {
fmt.Printf("modify i 為:%v,i的指針的內存地址為:%v\n",i,&i)
i = 11
}
----output----
main中 i 的值為:10,i 的內存地址為:0xc0420080b8,i 的指針的內存地址為:0xc042004028
modify i 為:10,i 的指針的內存地址為:0xc0420080d8
main中 i 的值為:10,i 的內存地址為:0xc0420080b8,i 的指針的內存地址為:0xc042004028
上面在函數接收的參數中沒有使用指針,所以在傳遞參數時,傳遞的是該值的副本,內存地址會改變,因此在函數中對該變量進行操作不會影響到原變量的值。
內存分布圖如下:
如果我將上面函數的參數傳遞方式改一下,改為接收參數的指針
func main() {
i := 10 //整形變量 i
ip := &i //指向整型變量 i 的指針ip,包含了 i 的內存地址
fmt.Printf("main中i的值為:%v,i 的內存地址為:%v,i的指針的內存地址為:%v\n",i,ip,&ip)
modifyBypointer(ip)
fmt.Printf("main中i的值為:%v,i 的內存地址為:%v,i的指針的內存地址為:%v\n",i,ip,&ip)
}
func modifyBypointer(i *int) {
fmt.Printf("modifyBypointer i 的內存地址為:%v,i的指針的內存地址為:%v\n",i,&i)
*i = 11
}
---output---
main中i的值為:10,i 的內存地址為:0xc042060080,i的指針ip的內存地址為:0xc042080018
modifyBypointer i 的內存地址為:0xc042060080,i的指針ip的內存地址為:0xc042080028
main中i的值為:11,i 的內存地址為:0xc042060080,i的指針ip的內存地址為:0xc042080018
將函數的參數改為傳遞指針后,函數內部對變量的修改就會影響到原變量的值,且不會影響到原變量的內存地址。但是可以看出main中各個參數的內存地址與函數中接收到的內存地址不一致,也就是說指針作為函數參數的傳遞過程中,是傳遞了該指針的副本地址,不是原指針地址。
那么既然函數中的指針地址與main中的指針地址不一致,那么我們在函數中對變量進行修改時,函數中對變量的修改又怎么會影響到main中原變量的值呢?
這是因為,雖然函數中的指針地址與main中的指針地址不一致,但是它們都指向同一個整形變量的內存地址,所以無論哪一方對變量i進行操作都會影響到變量i,且另一方是可以觀察到的。
我們來看一下這個內存分布圖
到目前為止,我們驗證了非引用類型和指針的參數傳遞都是傳遞副本,那么對于引用類型的參數傳遞又是如何的呢?
①映射map
我們使用make初始化一個映射map時,實際上返回的是該映射map的一個指針,具體源碼如下
// 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 {}
也就是說,對于引用類型map來講,實際上在作為傳遞參數時還是使用了指針的副本進行傳遞,屬于值傳遞。
②chan類型
使用make初始化 chan類型,底層其實跟map一樣,都是返回該值的指針
func makechan(t *chantype, size int) *hchan {}
③Slice類型
Slice類型對于之前的map,chan類型不太一樣,比如下面這個代碼示例
func main() {
i := []int{1,2,3}
fmt.Printf("i:%p\n",i)
fmt.Println("i[0]:",&i[0])
fmt.Printf("i:%v\n",&i)
}
---output---
i:0xc04205e0c0
i[0]: 0xc04205e0c0
i:&[1 2 3]
我們可以看到,使用&操作符表示slice的地址是無效的,而且使用%p輸出的內存地址與slice的第一個元素的地址是一樣的,那么為什么會出現這樣的情況呢?
我們來看一下在 fmt/print.go中的printValue函數源碼
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) //然后遞歸獲取vaule的內容
return
}
}
如果是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("i:%v\n",&i) 會輸出 i:&[1 2 3]的原因。
然后我們再來分析一下為什么使用%p輸出的內存地址與slice的第一個元素的地址是一樣的。
繼續看fmt/print.go中的 fmtPointer 源碼
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,Func等被當成指針處理,通過value.Pointer獲取對應的值的指針。
value.Pointer的源碼如下:
// 如果v的類型是Func,則返回的指針是底層代碼指針,但不一定足以唯一地標識單個函數。
// 唯一的保證是當且僅當v是nil func值時結果為零。
//
//如果v的類型是Slice,則返回的指針指向切片的第一個元素。
//如果切片為nil,則返回值為0。如果切片為空但非nil,則返回值為非零。
func (v Value) Pointer() uintptr {
k := v.kind()
switch k {
case Chan, Map, Ptr, UnsafePointer:
return uintptr(v.pointer())
case Func:
if v.flag&flagMethod != 0 {
f := methodValueCall
return **(**uintptr)(unsafe.Pointer(&f))
}
p := v.pointer()
// Non-nil func value points at data block.
// First word of data block is actual code.
if p != nil {
p = *(*unsafe.Pointer)(p)
}
return uintptr(p)
case Slice:
return (*SliceHeader)(v.ptr).Data
}
panic(&ValueError{"reflect.Value.Pointer", v.kind()})
}
所以當是slice類型的時候,fmt.Printf返回是slice這個結構體里第一個元素的地址。說到底,又轉變成了指針處理,只不過這個指針是slice中第一個元素的內存地址。之前說Slice類型對于之前的map,chan類型不太一樣,不一樣就在于slice是一種結構體+第一個元素指針的混合類型,通過元素array(Data)的指針,可以達到修改slice里存儲元素的目的。
type slice struct {
array unsafe.Pointer //這里的指針其實是第一個元素的指針
len int
cap int
}
根據slice與map,chan對比,我們可以總結一條規律:
可以通過某個變量類型本身的指針(如map,chan)或者該變量類型內部的元素的指針(如slice的第一個元素的指針)修改該變量類型的值。
因此slice也跟chan與map一樣,屬于值傳遞,傳遞的是第一個元素的指針的副本。
總結:在Go語言中只存在值傳遞(要么是該值的副本,要么是指針的副本),不存在引用傳遞。之所以對于引用類型的傳遞可以修改原內容數據,是因為在底層默認使用該引用類型的指針進行傳遞,但是也是使用指針的副本,依舊是值傳遞。
思考問題:
①既然slice是使用第一個元素的內存地址作為slice的指針,那么如果出現兩個相同的slice,它們的指針豈不會相同
②slice在作為參數傳遞時,可以修改原slice的數據,那么可以修改原slice的len和cap嗎