修改參數(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ù)。