值接收者和指針接收者#
指定了接收者的函數稱為方法。其中接收者可以是值接收者Animal
,也可以是指針接收者*Animal
:
type Animal struct {
name string
}
func (a Animal) valueFn() {} // 值接收者
func (a *Animal) ptrFn() {} // 指針接收者
在調用方法的時候,調用者可以是該類型的值類型也可以是指針類型。
- 值接收者方法不管調用者是指類型還是指針類型,傳遞的始終是調用者的副本,方法內部不會對調用者本身做修改:
// eg1.
func (a Animal) valueFn() {
a.name = "new_" + a.name
}
func main() {
a := Animal{"cat"} // 值類型
a.valueFn()
println("this is name", a.name)
// => this is name cat
b := &Animal{"dog"} // 指針類型
b.valueFn()
println("this is name", b.name)
// => this is name dog
}
- 指針接收者方法傳遞的是調用者的引用,所以可以對調用者做修改:
// eg2.
func (a *Animal) ptrFn() {
a.name = "new_" + a.name
}
func main() {
a := Animal{"cat"} // 值類型
a.ptrFn()
println("this is name", a.name)
// => this is name new_cat
b := &Animal{"dog"} // 指針類型
b.ptrFn()
println("this is name", b.name)
// => this is name new_dog
}
這背後是編譯器做了一些工作,如下表:
- | 值接收者 | 指針接收者 |
---|---|---|
值類型調用者 | 方法會使用調用者的一個副本,類似於 “傳值” | 使用值的引用來調用方法,eg2 實際上是 (&a).ptrFn() |
指針類型調用者 | 指針被解引用為值,eg1 實際上是 (*b).valueFn() | 實際上也是 “傳值”,方法裡的操作會影響到調用者,類似於指針傳參,拷貝了一份指針 |
類型的本質#
在聲明一個新類型之後,聲明一個該類型的方法之前,需要先回答一個問題:這個類型的本質是什麼。如果給這個類型增加或者刪除某個值,是要創建一個新值,還是要更改當前的值?如果是要創建一個新值,該類型的方法就使用值接收者。如果是要修改當前值,就使用指針接收者。這個答案也會影響程序內部傳遞這個類型的值的方式:是按值做傳遞,還是按指針做傳遞。保持傳遞的一致性很重要。這個背後的原則是,不要只關注某個方法是如何處理這個值,而是要關注這個值的本質是什麼。
是使用值接收者還是指針接收者,不應該由該方法是否修改了接收到的值來決定。這個決策應該基於該類型的本質。這條規則的一個例外是,需要讓類型值符合某個接口的時候,即便類型的本質是非原始本質的,也可以選擇使用值接收者聲明方法。這樣做完全符合接口值調用方法的機制。
—— 《Go 語言實戰 5.3》
- 內置類型:數值類型、字符串類型和布爾類型。這些類型本質上是原始的類型。因此,當對這些值進行增加或者刪除的時候,會創建一個新值。基於這個結論,當把這些類型的值傳遞給方法或者函數時,應該傳遞一個對應值的副本。
- 原始類型:切片、映射、通道、接口和函數類型。後邊沒看懂 😅
- 結構類型:用來描述一組值。遵循如有修改就按指針傳遞。如果類型的值具備非原始的本質,就應該被共享,而不是被複製。即使方法沒有修改接收者的值,依然要用指針接收者來聲明的。
這一節沒有看明白,大概是說對於非結構類型,應當參考標準庫的習慣;對於結構類型,一般情況下如果不需要對調用者做修改,就按值傳遞,如果需要修改則按指針傳遞。
接口#
雖然對不同類型的調用者,值接收者和指針接收者方法都可以調用,但在接口的實現上略有不同,實現接收者是值類型的方法,會隱式地實現了接收者是指針類型的方法,而實現接收者是指針類型的方法時,不會自動實現接收者是值類型的方法。
// eg3. 編譯通過
type IAnimal interface {
valueFn()
ptrFn()
}
func (a Animal) valueFn() {}
func (a *Animal) ptrFn() {}
func main() {
var a IAnimal = &Animal{"cat"}
a.valueFn()
a.ptrFn()
}
// eg4. 編譯失敗
func main() {
var a IAnimal = Animal{"cat"} // 改為值類型
a.valueFn()
a.ptrFn()
}
// => .\main.go:22:18: cannot use Animal{…} (value of type Animal) as IAnimal value in variable declaration: Animal does not implement IAnimal (method ptrFn has pointer receiver)
方法集#
要了解用指針接收者來實現接口時為什麼 user 類型的值無法實現該接口,需要先了解方法集。方法集定義了一組關聯到給定類型的值或者指針的方法。定義方法時使用的接收者的類型決定了這個方法是關聯到值,還是關聯到指針,還是兩個都關聯。
- 規範描述的方法集:
values | methods receivers |
---|---|
T | (t T) |
* T | (t T) and (t * T) |
意即 T 類型的值的方法集只包含值接收者聲明的方法。而指向 T 類型的指針的方法集既包含值接收者聲明的方法,也包含指針接收者聲明的方法。
- 接收者的角度來看方法集規範:
methods receivers | values |
---|---|
(t T) | T and * T |
(t * T) | * T |
這個視角是說,如果使用值接收者來實現一個接口,值類型值和指針都能夠實現對應的接口。如果使用指針接收者來實現一個接口,那麼只有指向那個類型的指針才能夠實現對應的接口。
所以 eg4 中的值類型 Animal
會提示沒有實現接口 IAnimal
。
嵌入類型#
嵌入類型是將已有的類型直接聲明在新的結構類型裡。被嵌入的類型被稱為新的外部類型的內部類型。
type Cat struct {
Animal
color string
}
func main() {
c := Cat{Animal{"miao"},"yellow"}
c.Animal.valueFn()
}
內部類型的屬性和方法也可以被提升到外部類型直接訪問:
func main() {
//...
c.valueFn()
println("this is c name", c.name)
}
參考#
- 《Go 語言實戰》第五章
- 值接收者和指針接收者的區別 | Go 程序員面試筆試寶典