banner
魔法少女秋小日

Qiuxiaori

github
email

Go 語言類型系統

值接收者和指針接收者#

指定了接收者的函數稱為方法。其中接收者可以是值接收者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 類型的值無法實現該接口,需要先了解方法集。方法集定義了一組關聯到給定類型的值或者指針的方法。定義方法時使用的接收者的類型決定了這個方法是關聯到值,還是關聯到指針,還是兩個都關聯。

  • 規範描述的方法集:
valuesmethods receivers
T(t T)
* T(t T) and (t * T)

意即 T 類型的值的方法集只包含值接收者聲明的方法。而指向 T 類型的指針的方法集既包含值接收者聲明的方法,也包含指針接收者聲明的方法。

  • 接收者的角度來看方法集規範:
methods receiversvalues
(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)
}

參考#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。