Golang接收者方法語法糖

人艱不拆_zmc發表於2023-05-16

1、概述

在《Golang常用語法糖》這篇博文中我們講解Golang中常用的12種語法糖,在本文我們主要講解下接收者方法語法糖。

在介紹Golang接收者方法語法糖前,先簡單說下Go 語言的指標 (Pointer),大致上理解如下:

  • 變數名前的 & 符號,是取變數的記憶體地址,不是取值;
  • 資料型別前的 * 符號,代表要儲存的是對應資料型別的記憶體地址,不是存值;
  • 變數名前的 * 符號,代表從記憶體地址中取值 (Dereferencing)。

注意 1:golang 指標詳細介紹請參見《Golang指標隱式間接引用》此篇博文。

2、接收者方法語法糖

在 Go 中,對於自定義型別 T,為它定義方法時,其接收者可以是型別 T 本身,也可能是 T 型別的指標 *T。

type Instance struct{}

func (ins *Instance) Foo() string {
 return ""
}

在上例中,我們定義了 Instance 的 Foo 方法時,其接收者是一個指標型別(*Instance)。

func main() {
 var _ = Instance{}.Foo() //編譯錯誤:cannot call pointer method on Instance{} ,變數是不可變的(該變數沒有地址,不能對其進行定址操作)
}

因此,如果我們用 Instance 型別本身 Instance{} 值去呼叫 Foo 方法,將會得到以上錯誤。

type Instance struct{}

func (ins Instance) Foo() string {
 return ""
}

func main() {
 var _ = Instance{}.Foo() // 編譯透過
}

此時,如果我們將 Foo 方法的接收者改為 Instance 型別,就沒有問題。

這說明,定義型別 T 的函式方法時,其接收者型別決定了之後什麼樣的型別物件能去呼叫該函式方法。但,實際上真的是這樣嗎?

type Instance struct{}

func (ins *Instance) String() string {
 return ""
}

func main() {
 var ins Instance
 _ = ins.String() // 編譯器會自動獲取 ins 的地址並將其轉換為指向 Instance 型別的指標_ = (&ins).String()
}

實際上,即使是我們在實現 Foo 方法時的接收者是指標型別,上面 ins 呼叫的使用依然沒有問題。

Ins 值屬於 Instance 型別,而非 *Instance,卻能呼叫 Foo 方法,這是為什麼呢?這其實就是 Go 編譯器提供的語法糖!

當一個變數可變時(也就是說,該變數是一個具有地址的變數,我們可以對其進行定址操作),我們對型別 T 的變數直接呼叫 *T 方法是合法的,因為 Go 編譯器隱式地獲取了它的地址。變數可變意味著變數可定址,因此,上文提到的 Instance{}.Foo() 會得到編譯錯誤,就在於 Instance{} 值不能定址。

注意 1:在 Go 中,即使變數沒有被顯式初始化,編譯器仍會為其分配記憶體空間,因此變數仍然具有記憶體地址。不過,由於變數沒有被初始化,它們在分配後僅被賦予其型別的預設零值,而不是初始值。當然,這些預設值也是儲存在變數分配的記憶體空間中的。

例如,下面的程式碼定義了一個整型變數 x,它沒有被顯式初始化,但是在分配記憶體時仍然具有一個地址:

var x int
fmt.Printf("%p\n", &x) // 輸出變數 x 的記憶體地址

輸出結果類似於:0xc0000120a0,表明變數 x 的記憶體地址已經被分配了。但是由於變數沒有被初始化,x 的值將為整型的預設值 0。  

3、深入測試

3.1 示例

package main

type B struct {
    Id int
}

func New() B {
    return B{}
}

func New2() *B {
    return &B{}
}

func (b *B) Hello() {
    return
}

func (b B) World() {
    return
}

func main() {
    // 方法的接收器為 *T 型別
    New().Hello() // 編譯不透過

    b1 := New()
    b1.Hello() // 編譯透過

    b2 := B{}
    b2.Hello() // 編譯透過

    (B{}).Hello() // 編譯不透過
    B{}.Hello()   // 編譯不透過

    New2().Hello() // 編譯透過

    b3 := New2()
    b3.Hello() // 編譯透過

    b4 := &B{} // 編譯透過
    b4.Hello() // 編譯透過

    (&B{}).Hello() // 編譯透過

    // 方法的接收器為 T 型別
    New().World() // 編譯透過

    b5 := New()
    b5.World() // 編譯透過

    b6 := B{}
    b6.World() // 編譯透過

    (B{}).World() // 編譯透過
    B{}.World()   // 編譯透過

    New2().World() // 編譯透過

    b7 := New2()
    b7.World() // 編譯透過

    b8 := &B{} // 編譯透過
    b8.World() // 編譯透過

    (&B{}).World() // 編譯透過
}

輸出結果:

./main.go:25:10: cannot call pointer method on New()
./main.go:25:10: cannot take the address of New()
./main.go:33:10: cannot call pointer method on B literal
./main.go:33:10: cannot take the address of B literal
./main.go:34:8: cannot call pointer method on B literal
./main.go:34:8: cannot take the address of B literal

3.2 問題總結

假設 T 型別的方法上接收器既有 T 型別的,又有 *T 指標型別的,那麼就不可以在不能定址的 T 值上呼叫 *T 接收器的方法
  • &B{} 是指標,可定址
  • B{} 是值,不可定址
  • b := B{} b是變數,可定址

4、總結 

在 Golang 中,當一個變數是可變的(也就是說,該變數是一個具有地址的變數,我們可以對其進行定址操作),我們可以透過對該變數的指標進行方法呼叫來執行對該變數的操作,否則就會導致編譯錯誤。

參考:Go 中的那些語法糖

參考:Go 挖坑指南: cannot take the address & cannot call pointer method

相關文章