【Go參數(shù)傳遞:值類型、引用類型和指針類型】

修改參數(shù)

func main() {
    p := person{name : "zhangsan", age : 18}
    modifyPerson(p)
    
    fmt.Println("name:", p.name, "age:", p.age)
}

func modifyPerson(p person) {
    p.name = "lisi"
    p.age = 19
}

type person struct {
    name string
    age int 
}

// 輸出結(jié)果
name:zhangsan, age:18    // 發(fā)現(xiàn)結(jié)果沒(méi)有被改變,我們改成指針類型試試呢?


modifyPerson(&p)
func modifyPerson(p *person){
    p.name = "lisi"
    p.age = 19
}

// 輸出結(jié)果
name: lisi, age: 19   // 接收參數(shù)修改為指針參數(shù),就可以滿足需求了。

值類型

上述示例中,定義的普通變量p是person類型的。在Go語(yǔ)言中,**person是一個(gè)值類型,而&p獲取的指針是*person類型的,即指針類型**。那么為什么值類型在參數(shù)傳遞中無(wú)法修改呢? 要從內(nèi)存講起。

變量的值是存儲(chǔ)在內(nèi)存中的,而內(nèi)存都有一個(gè)編號(hào),稱為內(nèi)存地址。所以要想修改內(nèi)存中的數(shù)據(jù),就要找到這個(gè)內(nèi)存地址。我們來(lái)對(duì)比值類型變量在函數(shù)內(nèi)外的內(nèi)存地址,如下
func main() {
    p := person{name : "zhangsan", age : 18}
    fmt.Println("main函數(shù),p的內(nèi)存地址:", &p)
    modifyPerson(p)
    
    fmt.Println("name:", p.name, "age:", p.age)
}

func modifyPerson(p person) {
    fmt.Println("modifyPerson 函數(shù)p的內(nèi)存地址:", &p)
    p.name = "lisi"
    p.age = 19
}

// 輸出結(jié)果
main函數(shù),p的內(nèi)存地址: 0x0000a3020
modifyPerson 函數(shù)p的內(nèi)存地址:0x0000a3040
name:zhangsan, age:18

// 我們發(fā)現(xiàn)內(nèi)存地址不一樣,意味著,在modifyPerson函數(shù)中修改的參數(shù)p和main函數(shù)中的變量p不是同一個(gè),這就是為什么我們?cè)趍odifyPerson函數(shù)中修改了參數(shù)p,但是在main函數(shù)中打印結(jié)果中沒(méi)有修改的原因
導(dǎo)致這種結(jié)果的原因是**Go語(yǔ)言中函數(shù)傳參都是值傳遞**。值傳遞值得是傳遞原來(lái)數(shù)據(jù)的一份拷貝,而不是原來(lái)的數(shù)據(jù)本身。
image-20211217101053089
調(diào)用modifyPerson函數(shù)傳遞變量p的時(shí)候,Go語(yǔ)言會(huì)拷貝一個(gè)p放在新的內(nèi)存中,這樣新的p的內(nèi)存地址就和原來(lái)的不一樣了,但是里面的值是一樣的。即**副本的意思**,變量中的數(shù)據(jù)一樣,但是內(nèi)存地址不一樣。

除了struct外,**浮點(diǎn)型、整型、字符串、布爾、數(shù)組,這些都是值類型**。

指針類型

指針類型的變量保存的值就是數(shù)據(jù)對(duì)應(yīng)的內(nèi)存地址,所以在函數(shù)參數(shù)傳遞是傳值的原則下,拷貝的值也是內(nèi)存地址。
func main() {
    p := person{name : "zhangsan", age : 18}
    fmt.Println("main函數(shù),p的內(nèi)存地址:", &p)
    modifyPerson(&p)
    
    fmt.Println("name:", p.name, "age:", p.age)
}

func modifyPerson(p *person) {
    fmt.Println("modifyPerson 函數(shù)p的內(nèi)存地址:", &p)
    p.name = "lisi"
    p.age = 19
}

// 輸出結(jié)果
main函數(shù),p的內(nèi)存地址: 0x0000a3020
modifyPerson 函數(shù)p的內(nèi)存地址:0x0000a3020
name:lisi, age:19
**指針類型的參數(shù)是永遠(yuǎn)可以修改原數(shù)據(jù)的,因?yàn)樵趨?shù)傳遞時(shí),傳遞的是內(nèi)存地址。**

提示: 值傳遞的是指針,即內(nèi)存地址。通過(guò)內(nèi)存地址可以找到元數(shù)據(jù)的那塊內(nèi)存,所以修改它也就等于修改了原數(shù)據(jù)。

引用類型

引用類型,包括map、slice和chan。

**map**
func main() {
    m := make(map[string]int)
    m["奔跑的蝸牛"] = 18
    fmt.Println("age:" : m["奔跑的蝸牛"])
    modifyMap(m)
    fmt.Println("age:" : m["奔跑的蝸牛"])
}

func modifyMap(m map[string]int) {
    p["奔跑的蝸牛"] = 19
}

// 輸出結(jié)果
age:18
age:19

// 我們發(fā)現(xiàn)修改成功了。為什么沒(méi)有使用指針,只是使用了map類型的參數(shù),按照Go語(yǔ)言值傳遞原則,modifyMap函數(shù)中map是一個(gè)副本,為什么修改成功了呢?
一切原因要從 **make 這個(gè)Go語(yǔ)言內(nèi)建的函數(shù)說(shuō)起**。在Go語(yǔ)言中,任何創(chuàng)建map的代碼(不管是字面量還是make函數(shù))最終調(diào)用的都是 **runtime.makemap函數(shù)**。

提示: 用字面量或者make函數(shù)的方式創(chuàng)建map,并轉(zhuǎn)換成 makemap 函數(shù)的調(diào)用,這個(gè)轉(zhuǎn)換是Go語(yǔ)言編譯器自動(dòng)幫我們做的。

func makmap(t *maptype, hint int, h*hmap) *hamp{
    
}
從源碼可以看出,Go語(yǔ)言的map類型本質(zhì)上就是 *hmap,所以根據(jù)替換原則,`modifyMap(a map) 函數(shù)其實(shí)就是modifyMap(a *hmap)`。 這就和上面的指針類型的參數(shù)調(diào)用一樣了,也就是通過(guò)map類型的參數(shù)可以修改原始數(shù)據(jù)的原因,因?yàn)楸举|(zhì)上就是指針。

為了驗(yàn)證創(chuàng)建的map是一個(gè)指針,修改上述示例,
func main() {
    m := make(map[string]int)
    m["奔跑的蝸牛"] = 18
    fmt.Println("age:" : m["奔跑的蝸牛"])
    fmt.Println("main函數(shù)內(nèi)存地址:", m)
    modifyMap(m)
    fmt.Println("age:" : m["奔跑的蝸牛"])
}

func modifyMap(m map[string]int) {
    p["奔跑的蝸牛"] = 19
    fmt.Println("modifyMap函數(shù)內(nèi)存地址:", m)
}

// 輸出結(jié)果
age:18
main函數(shù)內(nèi)存地址:0x000060170
age:19
modifyMap函數(shù)內(nèi)存地址:0x000060170

// 從輸出結(jié)果看,內(nèi)存地址一模一樣,所以可以修改原始數(shù)據(jù)。而且在打印指針的時(shí)候,直接使用的是變量m和p,并沒(méi)有用取地址符&,因?yàn)樗麄儽臼【褪侵羔?,所以沒(méi)必要在使用&取地址了
Go語(yǔ)言通過(guò)make函數(shù)或字面量的包裝為我們省去了指針的操作,讓我們可以更容易的使用map。其實(shí)就是**語(yǔ)法糖**,這是編程界的老傳統(tǒng)了。

注意: 這里的map可以理解為引用類型, 但是它本質(zhì)是指針,只是可以叫做引用類型而已。在參數(shù)傳遞時(shí),它還是值傳遞,并不是其他編程語(yǔ)言中所謂的引用傳遞。

chan

channel也可以理解為引用類型, 而它本質(zhì)也是一個(gè)指針。
func makechan(t *chantype, size int64) *hchan{}

// 從源碼可以看到,所創(chuàng)建的chan其實(shí)是個(gè)*hchan,所以它在參數(shù)傳遞中也和map一樣
**嚴(yán)格的說(shuō),Go語(yǔ)言沒(méi)有引用類型**,但是我們可以把map、chan稱為引用類型,這樣便于理解。除了map、chan外,Go語(yǔ)言中的函數(shù)、接口、slice切片都可以稱為引用類型。

類型零值

在Go語(yǔ)言中,定義變量要么通過(guò)聲明、要么通過(guò)make和new函數(shù),不一樣的是make和new函數(shù)屬于顯示聲明并初始化。如果我們聲明的變量沒(méi)有顯示聲明初始化,那么該變量的默認(rèn)值就是對(duì)應(yīng)類型的零值。
類型 零值
數(shù)值類型(int、float) 0
bool false
string ""(空字符串)
struct 內(nèi)部字段零值
slice nil
map nil
指針 nil
函數(shù) nil
chan nil
interface nil

總結(jié):在Go語(yǔ)言中,函數(shù)的參數(shù)傳遞只有值傳遞,而且傳遞的實(shí)參都是原始數(shù)據(jù)的一份拷貝。如果拷貝的內(nèi)容是值類型的,那么在函數(shù)中無(wú)法修改原始數(shù)據(jù),如果拷貝的內(nèi)容是指針(或者可以理解為引用類型),那么可以在函數(shù)中修改原始數(shù)據(jù)。


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容