當實作某一個介面的方法的時候,會視是否要 shared state 來決定要用 Pointer Receiver 還是 Value Receiver。 而使用 Pointer Receiver 的時候,會發現不能使用 Value Type 傳給 Interface Value,會出現下面這樣的錯誤。

Cannot use ‘u’ (type user) as type notifier Type does not implement ‘notifier’ as ‘notify’ method has a Pointer Receiver

但是使用 Value Receiver 卻兩種都可以傳,這是為什麼呢?

例如下圖所示,使用 Value Receiver 可以使用 Pointer TypeValue Type

Value ReceiverValue Receiver

使用 Pointer Receiver 只能使用 Pointer Type 而已。 使用 Value Type 就會出現編譯錯誤。
Pointer ReceiverPointer Receiver

詞彙說明

因為學習 Go 的中文資源不多,此篇的內容也都是參考原文,很多詞彙不知道怎麼翻成中文,所以我先說明比較無法理解的英文詞彙。

詞彙 說明
User-Defined Type 像是 type user struct{} 或是 type myint int ,這些都算是 User-Defined Type 。
Method Sets 定義在 User-Defined Type 上的多個方法,如: func (u user) pretty() string {...}
Pointer Receiver 使用 Pointer 當作 Method 的 Receiver 如:func (u *user) pretty() string {...}
Value Receiver 使用 Value 當作 Method 的 Receiver 如:func (u user) pretty() string {...}
Value Type 使用 Value 的宣告方式,如: u := user{}
Pointer Type 使用 Pointer 的宣告方式,如: u := &user{}
Interface Value 宣告成介面的變數,例如範例中 notifier 是介面,而 n 就是 Interface Value var n notifier

介面(Interface) 運作機制

首先我們要先了解介面的運作機制。 介面是用來定義行為的一種 type,而你可以定義一個 User-Defined Type 實作介面定義好的行為(method)。 當你 assign User-Defined Type 給 Interface ValueInterface Value 會有兩個區塊用來紀錄 Stored Value 和 User-Defined Type info。 而 assign Value Type to Interface Value 跟 assign Pointer Type to Interface Value 紀錄的東西不太一樣。

Interface Value 會有兩區塊用來儲存資料,第一個區塊用來儲存叫做 iTable 的位址,而 iTable 儲存 User-Defined Type 的資訊和跟此介面有關聯的 Method Sets。 第二個區塊也是一個位址指向 Stored Value。 如下圖 assign Value Type user{"Bill"}Interface Value 的資料示意圖,此時 iTable 會儲存 Value Type Type(user) 的資訊。
Ref: Go in ActionRef: Go in Action

下圖是 assign Pointer Type &user{"Bill"}Interface Value 的資料示意圖。 跟上一張圖的差別是 iTable 是儲存指標的 Pointer Type Type(*user) 的資訊。

Ref: Go in ActionRef: Go in Action

Interface Value 執行方法時,就是用儲存於 Intreface Value 那兩區塊的資訊來執行方法。

Method Sets

從上述的說明,知道 Interface Value 執行的方法的機制後,接下來就是要了解為什麼 Method Sets 在使用 Pointer Receiver 實作介面後,在執行方法的內部運作機制。

接下來請看下列程式碼,我使用 Pointer Receiver 定義介面的方法,用 Value Type assign 給介面後,產生最開頭說到的編譯錯誤的情境。 (線上範例 https://play.golang.org/p/ojGulwuKZqo)

fail to compile
  • go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

type notifier interface {
notify()
}

type user struct {
name string
email string
}

func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}

func main() {
u := user{}
var n notifier = u
}

我們接著看 Go 語言的規格書,裡面有提到一個規格。

Method Sets
A type may have a method set associated with it. The method set of an interface type is its interface. The method set of any other type T consists of all methods declared with receiver type T. The method set of the corresponding Pointer Type T is the set of all methods declared with receiver T or T (that is, it also contains the method set of T). Further rules apply to structs containing embedded fields, as described in the section on struct types. Any other type has an empty method set. In a method set, each method must have a unique non-blank method name.

The method set of a type determines the interfaces that the type implements and the methods that can be called using a receiver of that type.

上面畫起來的重點,就如下列的 table 的說明。 如果你是用 Pointer Receiver 實作介面,則只有 Pointer 實作該介面。 如果是用 Value Receiver 實作介面,則 Value TypePointer Type 都有實作該介面。

Methods Receivers Values
(t T) T and *T
(t *T) *T

為什麼會有這種規格限制呢?

根據我參考 Go in Action - 5.4.3 Method Sets 的說明,書上說:「並不是每一次都可以從 Value 取得 位址」。 例如下面程式碼, 會出現 cannot take the address of duration(40) 的編譯錯誤。 (線上範例 https://play.golang.org/p/aas5NLV4dTg )

cannot get address
  • go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

type duration int

func (d *duration) pretty() string {
return fmt.Sprintf("Duration: %d", d)
}

func main() {
// cannot call pointer method on duration(40)
// cannot take the address of duration(40)
duration(40).pretty()
}

接著再參考 Why do T and *T have different method sets? 的解釋,如果 Interface ValueiTable 是指向 Pointer Type 的情況時,雖然在呼叫該方法的時候,可以 dereferencing pointer,但是如果 Interface ValueiTable 是指向 Value Type 將會沒有一個安全的解法,執行包含 pointer 的 Method Sets。 如果這樣做的話,將可以更改 Interface Value 裡面的值,這在 GO 的規格上是不被允許的。

示意圖示意圖

另一個說法,如果我們允許這件事情發生的話,會發生什麼事情? 請參考下面的程式碼的例子。

如果允許傳入 Value Type 的話,UpperCaseName() 執行後,會造成 Name 不會真的更改到 user 上,這並不是我們要預期的行為。


NameOfTheCodeBlock
  • go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Changer interface {
UpperCaseName ()
}

type User struct {
Name string
}

func (u *User) UpperCaseName() {
u.Name = strings.ToUpper(u.Name)
}

func main() {
user := User{Name: "miles"}
var c Changer = &user // 如果允許傳入 Value Type 的話,UpperCaseName() 執行後,會造成 Name 不會真的更改到 user 上
c.UpperCaseName()
fmt.Println(user.Name) // 因為是傳入 Pointer Type,所以會印出大寫的 MILES
}

小結

但是為了搞清楚為什麼會有這種規格,我也參考了很多書籍才得出這樣的結論,最終在 Go Doc 的 FAQ 的這篇 Why do T and *T have different method sets? 找到滿意的解答 ,但是要看懂這個解答之前,也要先懂前面提到的介面的運作機制才行。

參考來源

[Why do T and *T have different method sets?]
[Go in Action]
[The Go Programming Language Specification]
[Mastering Go - Second Edition]
[The Way to Go: A Thorough Introduction to the Go Programming Language]
[Why can't I get the address of a type conversion in Go?]