Go面試必考題目之method篇

joy發表於2019-05-22

  在 Go 的類方法中,分為值接收者方法和指標接收者方法,對於剛開始接觸 Go 的同學來說,有時對 Go 的方法會感到困惑。下面我們結合題目來學習 Go 的方法。

  為了方便敘述,下文描述的值接收者方法簡寫為值方法,指標接收者方法簡寫為指標方法。

  下面程式碼中,哪段編號的程式碼會報錯?具體報什麼錯誤?

type Animal interface {
    Bark()
}

type Dog struct {
}

func (d Dog) Bark() {
    fmt.Println("dog")
}

type Cat struct {
}

func (c *Cat) Bark() {
    fmt.Println("cat")
}

func Bark(a Animal) {
    a.Bark()
}

func getDog() Dog {
    return Dog{}
}

func getCat() Cat {
    return Cat{}
}

func main() {
    dp := &Dog{}
    d := Dog{}
    dp.Bark() // (1)
    d.Bark()  // (2)
    Bark(dp)  // (3)
    Bark(d)   // (4)

    cp := &Cat{}
    c := Cat{}
    cp.Bark() // (5)
    c.Bark()  // (6)
    Bark(cp)  // (7)
    Bark(c)   // (8)

    getDog().Bark() // (9)
    getCat().Bark() // (10)
}

  拋磚引玉,讓我們學習完再來作答。

值方法和指標方法

我們來看看值方法的宣告。

type Dog struct {
}

func (d Dog) Bark() {
    fmt.Println("dog")
}

上面程式碼中,方法Bark的接收者是值型別,那麼這就是一個值接收者的方法。

下面再看看指標接收者的方法。

type Cat struct {
}

func (c *Cat) Bark() {
    fmt.Println("cat")
}

類的方法集合

這個在 Go 文件裡有定義:

  • 對於型別T,它的方法集合是所有接收者為T的方法。
  • 對於型別*T,它的方法集合是所有接收者為*TT的方法。
Values Method Sets
T (t T)
*T (t T) and (t *T)

方法的呼叫者

   指標*T接收者方法 :只有指標型別*T才能呼叫,但其實值T型別也能呼叫,為什麼呢?因為當使用值呼叫t.Call()時,Go 會轉換成(&t).Call(),也就是說最後呼叫的還是接收者為指標*T的方法。

  但要注意 t 是要能取地址才能這麼呼叫,比如下面這種情況就不行:

func getUser() User {
    return User{}
}

...

getUser().SayWat()
// 編譯錯誤:
// cannot call pointer method on aUser()
// cannot take the address of aUser()

  T接收者方法: 指標型別*T和值T型別都能呼叫。 Methods Receivers | Values ----|---- (t T) | T and *T (t *T) | *T

  使用接收者為*T的方法實現一個介面,那麼只有那個型別的指標*T實現了對應的介面。

  如果使用接收者為T的方法實現一個介面,那麼這個型別的值T和指標*T都實現了對應的介面。

宣告建議

  在給類宣告方法時,方法接收者的型別要統一,最好不要同時宣告接收者為值和指標的方法,這樣容易混淆而不清楚到底實現了哪些介面。

  下面我們來看看哪種型別適合宣告接收者為值或指標的方法。

指標接收者方法

下面這 2 種情況請務必宣告指標接收者方法:

  • 方法中需要對接收者進行修改的。
  • 類中包含sync.Mutex或類似鎖的變數,因為它們不允許值拷貝。

下面這 2 種情況也建議宣告指標接收者方法:

  • 類成員很多的,或者大陣列,使用指標接收者效率更高。
  • 如果拿不準,那也宣告接收者為指標的方法吧。

值接收者方法

下面這些情況建議使用值接收者方法:

  • 型別為mapfuncchannel
  • 一些基本的型別,如intstring
  • 一些小陣列,或小結構體並且不需要修改接收者的。

題目解析

type Animal interface {
    Bark()
}

type Dog struct {
}

func (d Dog) Bark() {
    fmt.Println("dog")
}

type Cat struct {
}

func (c *Cat) Bark() {
    fmt.Println("cat")
}

func Bark(a Animal) {
    a.Bark()
}

func getDog() Dog {
    return Dog{}
}

func getCat() Cat {
    return Cat{}
}

func main() {
    dp := &Dog{}
    d := Dog{}
    dp.Bark() // (1) 通過
    d.Bark()  // (2) 通過
    Bark(dp)
    // (3) 通過,上面說了型別*Dog的方法集合包含接收者為*Dog和Dog的方法
    Bark(d)   // (4) 通過

    cp := &Cat{}
    c := Cat{}
    cp.Bark() // (5) 通過
    c.Bark()  // (6) 通過
    Bark(cp)  // (7) 通過
    Bark(c)
    // (8) 編譯錯誤,值型別Cat的方法集合只包含接收者為Cat的方法
    // 所以T並沒有實現Animal介面

    getDog().Bark() // (9) 通過
    getCat().Bark()
    // (10) 編譯錯誤,
    // 上面說了,getCat()是不可地址的
    // 所以不能呼叫接收者為*Cat的方法
}

總結

  • 理清型別的方法集合。
  • 理清接收者方法的呼叫範圍。

參考文獻

感謝閱讀,歡迎大家指正,留言交流~

更多原創文章乾貨分享,請關注公眾號
  • Go面試必考題目之method篇
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章