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