天天看點

Go 結構體方法指針和值的差別

衆所周知,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 本身的底層實作。

總結

在這篇文章終于結束之前,總結一下這個無聊的實驗對我們的實際開發有哪些有意義的提示:

如果某個方法你需要對屬性做臨時修改(比如目前方法需要調用其他方法,而目标方法會讀取屬性值且你不被允許修改目标方法),那麼你應該将這個方法定義為值傳遞的

如果你某個方法定義為值傳遞的了,那麼切記,你在這個方法内直接或者套娃所做的一切修改都不會不會向上傳遞(作用到調用者那裡),但是它會向下傳遞

源碼來自于 程式設計寶庫。