衆所周知,Go struct 定義方法時使用指針還是值的差別就是在方法内修改屬性值時,用值定義的方法所做的修改隻限于方法内,而指針則沒有這個局限。
文章如果到這裡就結束了,那麼就很平平無奇了,于是我打算帶大家去做個無聊但是值得思考的實驗。
在開始之前,先寫段簡單的代碼跑一下前面說到的東西,順便讓大家熟悉一下接下來實驗代碼的一些編碼規則,哦對了,以下代碼寫于 2021.08,Go 版本是 1.16.5,如果你看到這篇文章的時候 Go 已經更新了很多個版本了,可能就不适用了。廢話不多說,上代碼:
package main
import "fmt"
type Foo struct {
val int
}
/**
* 在這裡,我定義了兩個 Set 方法,一個 P 結尾,一個 V 結尾,聰明的你肯定很快就反應過來了:
* P 即 Pointer,V 即 Value
*
* 另外我在這裡加了個 callBy,友善追蹤調用鍊
*/
func (f *Foo) SetP(v int, callBy string) {
f.val = v
fmt.Printf("In SetP(): call by:%s\tval:%d\n", callBy, f.val)
}
func (f Foo) SetV(v int, callBy string) {
f.val = v
fmt.Printf("In SetV(): call by:%s\tval:%d\n", callBy, f.val)
}
func main() {
f := Foo{0}
fmt.Printf("In main(): val:%d\n", f.val)
fmt.Println("=====================================")
f.SetP(1, "main")
fmt.Printf("In main(): after f.SetP(1): val:%d\n", f.val)
fmt.Println("=====================================")
f.SetV(2, "main")
fmt.Printf("In main(): after f.SetV(2): val:%d\n", f.val)
fmt.Println("=====================================")
}
運作結果:
In main(): val:0
=====================================
In SetP(): call by:main val:1
In main(): after f.SetP(1): val:1
=====================================
In SetV(): call by:main val:2
In main(): after f.SetV(2): val:1
如我們預期,通過值定義的方法内對屬性的修改并不會把影響帶到外部。
接下來,開始我們的實驗
假如方法套娃,會發生什麼?
在我們日常開發時,經常會遇到方法裡調用另一個方法,那假如被調用的方法裡修改了屬性,會發生什麼呢?
套娃會有四種情況:PV、VP、VV、PP(實際情況可能還會出現更多層的套娃,但是這裡我們隻需要弄懂一層的,剩下可以按照數學歸納法去了解。),往代碼中加四個 Set 方法:
func (f *Foo) SetPV(v int, callBy string) {
f.SetV(v+1, callBy+"->SetPV")
fmt.Printf("In SetPV(): call by:%s\tval:%d\n", callBy, f.val)
f.val = v
}
func (f Foo) SetVP(v int, callBy string) {
f.SetP(v+1, callBy+"->SetVP")
fmt.Printf("In SetVP(): call by:%s\tval:%d\n", callBy, f.val)
f.val = v
}
func (f *Foo) SetPP(v int, callBy string) {
f.SetP(v+1, callBy+"->SetPP")
fmt.Printf("In SetPP(): call by:%s\tval:%d\n", callBy, f.val)
f.val = v
}
func (f Foo) SetVV(v int, callBy string) {
f.SetV(v+1, callBy+"->SetVV")
fmt.Printf("In SetVV(): call by:%s\tval:%d\n", callBy, f.val)
f.val = v
}
然後在 main() 裡加上:
func main() {
f := Foo{0}
fmt.Printf("In main(): val:%d\n", f.val)
fmt.Println("=====================================")
f.SetP(1, "main")
fmt.Printf("In main(): after f.SetP(1): val:%d\n", f.val)
fmt.Println("=====================================")
f.SetV(2, "main")
fmt.Printf("In main(): after f.SetV(2): val:%d\n", f.val)
fmt.Println("=====================================")
f.SetPV(3, "main")
fmt.Printf("In main(): after f.SetPV(3): val:%d\n", f.val)
fmt.Println("=====================================")
f.SetVP(4, "main")
fmt.Printf("In main(): after f.SetVP(4): val:%d\n", f.val)
fmt.Println("=====================================")
f.SetVV(5, "main")
fmt.Printf("In main(): after f.SetVV(5): val:%d\n", f.val)
fmt.Println("=====================================")
f.SetPP(6, "main")
fmt.Printf("In main(): after f.SetPP(6): val:%d\n", f.val)
}
執行結果:
In main(): val:0
=====================================
In SetP(): call by:main val:1
In main(): after f.SetP(1): val:1
=====================================
In SetV(): call by:main val:2
In main(): after f.SetV(2): val:1
=====================================
In SetV(): call by:main->SetPV val:4
In SetPV(): call by:main val:1
In main(): after f.SetPV(3): val:3
=====================================
In SetP(): call by:main->SetVP val:5
In SetVP(): call by:main val:5
In main(): after f.SetVP(4): val:3
=====================================
In SetV(): call by:main->SetVV val:6
In SetVV(): call by:main val:3
In main(): after f.SetVV(5): val:3
=====================================
In SetP(): call by:main->SetPP val:7
In SetPP(): call by:main val:7
In main(): after f.SetPP(6): val:6
列個表格:
方法 main() 調用結束時 f.val 值 第一層方法名/第二層方法結束時 f.val 值 第二層方法名/方法結束時 f.val 值
SetPV() 3 SetPV(3) / 1 SetV(3+1) / 4
SetVP() 3 SetVP(4) / 5 SetP(4+1) / 5
SetVV() 3 SetVV(5) / 3 SetV(5+1) / 6
SetPP() 6 SetPP(6) / 7 SetP(6+1) / 7
得出結論:隻有整個調用鍊路都是用指針定義的方法,對屬性做的修改才會保留,否則隻會在方法内有效,符合最開始說的規則。
到這裡你可能以為文章就要結束了,但是并沒有,我們重點關注一下 SetVP():
func (f Foo) SetVP(v int, callBy string) {
f.SetP(v+1, callBy+"->SetVP") // 看這裡,這裡可是指針喔,為什麼它修改的值,也僅限于 SetVP() 内呢
fmt.Printf("In SetVP(): call by:%s\tval:%d\n", callBy, f.val)
f.val = v
}
把長得很像的 SetPP() 修改一下:
func (f *Foo) SetPP(v int, callBy string) {
f.SetP(v+1, callBy+"->SetPP") // 這裡也是指針
fmt.Printf("In SetPP(): call by:%s\tval:%d\n", callBy, f.val)
// f.val = v /* 注釋掉了這一行 */
}
執行它之後,它修改的值卻不僅僅是在 SetPP() 内部!難道 (f Foo) 會導緻内部的 (f *Foo) 方法也拷貝了一份?
把指針列印出來确認一下!
func (f *Foo) SetP(v int, callBy string) {
fmt.Printf("In SetP(): &f=%p\t&f.val=%p\n", &f, &f.val)
f.val = v
fmt.Printf("In SetP(): call by:%s\tval:%d\n", callBy, f.val)
}
// ... 省略其他方法的修改,都是一樣的,隻是換個名字而已
func (f Foo) SetVP(v int, callBy string) {
fmt.Printf("In SetVP(): &f=%p\t&f.val=%p\n", &f, &f.val)
f.SetP(v+1, callBy+"->SetVP")
fmt.Printf("In SetVP(): call by:%s\tval:%d\n", callBy, f.val)
f.val = v
}
func main() {
f := Foo{0}
fmt.Printf("In main(): val:%d\n", f.val)
// ... 省略其他沒有修改的地方
}
看看運作結果(我标記了需要重點關注的那三行):
In main(): val:0
⚠️ In main(): &f=0x14000124008 &f.val=0x14000124008
====================================================
In SetP(): &f=0x14000126020 &f.val=0x14000124008
In SetP(): call by:main val:1
In main(): after f.SetP(1): val:1
====================================================
In SetV(): &f=0x14000124010 &f.val=0x14000124010
In SetV(): call by:main val:2
In main(): after f.SetV(2): val:1
====================================================
In SetPV(): &f=0x14000126028 &f.val=0x14000124008
In SetV(): &f=0x14000124018 &f.val=0x14000124018
In SetV(): call by:main->SetPV val:4
In SetPV(): call by:main val:1
In main(): after f.SetPV(3): val:3
====================================================
⚠️ In SetVP(): &f=0x14000124030 &f.val=0x14000124030
⚠️ In SetP(): &f=0x14000126030 &f.val=0x14000124030
In SetP(): call by:main->SetVP val:5
In SetVP(): call by:main val:5
In main(): after f.SetVP(4): val:3
====================================================
In SetVV(): &f=0x14000124038 &f.val=0x14000124038
In SetV(): &f=0x14000124060 &f.val=0x14000124060
In SetV(): call by:main->SetVV val:6
In SetVV(): call by:main val:3
In main(): after f.SetVV(5): val:3
====================================================
In SetPP(): &f=0x14000126038 &f.val=0x14000124008
In SetP(): &f=0x14000126040 &f.val=0x14000124008
In SetP(): call by:main->SetPP val:7
In SetPP(): call by:main val:7
In main(): after f.SetPP(6): val:6
可以發現:
不管是 (f Foo) 還是 (f *Foo),在方法内部,f 本身都是拷貝的
屬性位址
2.1. 如果是指針方法,則屬性位址繼承于調用方
2.2. 如果是值方法,則屬性位址是新開辟的空間位址
至于說套娃調用多了會不會導緻記憶體飙升,這裡就不展開讨論了,有興趣的可以去自己查閱資料或者看看 Go 本身的底層實作。
總結
在這篇文章終于結束之前,總結一下這個無聊的實驗對我們的實際開發有哪些有意義的提示:
如果某個方法你需要對屬性做臨時修改(比如目前方法需要調用其他方法,而目标方法會讀取屬性值且你不被允許修改目标方法),那麼你應該将這個方法定義為值傳遞的
如果你某個方法定義為值傳遞的了,那麼切記,你在這個方法内直接或者套娃所做的一切修改都不會不會向上傳遞(作用到調用者那裡),但是它會向下傳遞
源碼來自于 程式設計寶庫。