golang的值接收者和指針接收者的區別
方法
方法能給用戶自定義的類型添加新的行爲。方法和函數的區別在於方法有一個接收者,給一個函數添加一個接收者,那麼它就變成了方法。接收者可以是值接收者,也可以是指針接收者。
我們在調用方法的時候,值類型既可以調用值接收者的方法,也可以調用指針接收者的方法;
指針類型既可以調用指針接收者的方法,也可以調用值接收者的方法。
也就是說,不管方法的接收者是什麼類型,該類型的值和指針都可以調用,不必嚴格符合接收者的類型。下面的一個實例可以驗證這個結論:
package main
import "fmt"
type Person struct {
name string
age int
}
func (p Person) howOld() int {
fmt.Printf("internal howOld, addrss is %p \n", &p)
return p.age
}
func (p Person) howOld2() int {
fmt.Printf("internal howOld2, addrss is %p \n", &p)
return p.age
}
func (p *Person) growUp() {
fmt.Printf("internal growUp, addrss is %p \n", p)
p.age += 1
}
func main() {
yuting := Person{
name: "yuting",
age: 18,
}
fmt.Printf("person, addrss is %p \n", &yuting)
yuting.howOld()
yuting.howOld2()
yuting.growUp()
fmt.Printf("person, addrss is %p \n", &yuting)
fmt.Println("------------- 2 -------------")
yt := &Person{
name: "yuting",
age: 18,
}
fmt.Printf("person, addrss is %p \n", yt)
yt.howOld()
yt.howOld2()
yt.growUp()
fmt.Printf("person, addrss is %p \n", yt)
}
我們看看這個實例的執行結果:
person, addrss is 0xc00000c040
internal howOld, addrss is 0xc00008a020
internal howOld2, addrss is 0xc00000c060
internal growUp, addrss is 0xc00000c040
person, addrss is 0xc00000c040
------------- 2 -------------
person, addrss is 0xc00000c080
internal howOld, addrss is 0xc00000c0a0
internal howOld2, addrss is 0xc00008a040
internal growUp, addrss is 0xc00000c080
person, addrss is 0xc00000c080
從打印出的地址可以看出:
- 對於值接收者,如果調用者也是值對象,那麼會將調用者的值拷貝一份,並執行方法,方法的調用不會影響到調用者值。 如果調用者是指針對象,那麼會解引用指針對象爲值,然後將解引的對象拷貝一份,然後 執行方法。
- 對於指針接收者,如果調用者是值對象,會使用值的引用來調用方法,上例中,
yuting.growUp()
實際上是 、(&yuting).growUp()
, 所以傳入指針接收者方法的對象地址和 調用者地址一樣。如果調用者是指針對象,實際上也是“傳值”,方法裏的操作會影響到調用者,類似於指針傳參,拷貝了一份指針,但是指針指向同一個對象。
用一個表格來表示:
值接受者 | 指針接收者 | |
---|---|---|
值調用者 | 方法會使用調用者的一個副本,類似於“傳值“ | 使用值的引用來調用方法,上例中,yuting.growUp() 實際上是 (&yuting).growUp() |
指針調用者 | 指針被解引用爲值,上例中,yt.howOld() 實際上是 (*yt).howOld() | 實際上也是“傳值”,方法裏的操作會影響到調用者,類似於指針傳參,拷貝了一份指針 |
值接收者和指針接收者
前面說過,不管接收者類型是值類型還是指針類型,都可以通過值類型或指針類型調用,這裏面實際上通過語法糖起作用的。
先說結論:實現了接收者是值類型的方法,相當於自動實現了接收者是指針類型的方法;而實現了接收者是指針類型的方法,不會自動生成對應接收者是值類型的方法。
先來看個例子:
package main
import "fmt"
type coder interface {
howOld() int
howOld2() int
growUp()
}
type Gopher struct {
name string
age int
}
func (p Gopher) howOld() int {
fmt.Printf("internal howOld, addrss is %p \n", &p)
return p.age
}
func (p Gopher) howOld2() int {
fmt.Printf("internal howOld2, addrss is %p \n", &p)
return p.age
}
func (p *Gopher) growUp() {
fmt.Printf("internal growUp, addrss is %p \n", p)
p.age += 1
}
func main() {
fmt.Println("------------- 1 -------------")
var yt coder = &Gopher{
name: "yuting",
age: 18,
}
fmt.Printf("person, addrss is %p \n", yt)
yt.howOld()
yt.howOld2()
yt.growUp()
fmt.Printf("person, addrss is %p \n", yt)
fmt.Println("------------- 2 -------------")
var yt coder = Gopher{
name: "yuting",
age: 18,
}
//fmt.Printf("person, addrss is %p \n", &yuting)
//yuting.howOld()
//yuting.howOld2()
//yuting.growUp()
//fmt.Printf("person, addrss is %p \n", &yuting)
}
上面定義了一個接口:
type coder interface {
howOld() int
howOld2() int
growUp()
}
定義了結構體:Gopher,它實現了兩個方法,兩個值接收者,一個指針接收者。最後,我們在 main 函數裏通過接口類型的變量(Gopher指針)調用了定義的兩個函數。運行正常。
但是如果我們把 coder interface 變量的值改成 Gopher 實例,就會編譯失敗,報錯:
cannot use Gopher literal (type Gopher) as type coder in assignment:
Gopher does not implement coder (growUp method has pointer receiver)
提示顯示:Gopher 對象沒有實現coder 接口的 growUp 方法,因爲growUp有一個指針接收者。
簡單的說就是,*Gopher 實現了coder, 但是Gopher沒有實現 coder。
有一個簡單的解釋:接收者是指針類型的方法,很可能在方法中會對接收者的屬性進行更改操作,從而影響接收者;而對於接收者是值類型的方法,在方法中不會對接收者本身產生影響。所以,當實現了一個接收者是值類型的方法,就可以自動生成一個接收者是對應指針類型的方法,因爲兩者都不會影響接收者。但是,當實現了一個接收者是指針類型的方法,如果此時自動生成一個接收者是值類型的方法,原本期望對接收者的改變(通過指針實現),現在無法實現,因爲值類型會產生一個拷貝,不會真正影響調用者。
結論就是:如果實現了接收者是值類型的方法,會隱含地也實現了接收者是指針類型的方法。
兩者分別在何時使用
如果方法的接收者是值類型,無論調用者是對象還是對象指針,修改的都是對象的副本,不影響調用者;如果方法的接收者是指針類型,則調用者修改的是指針指向的對象本身。
使用指針作爲方法的接收者的理由:
- 方法能夠修改接收者指向的值。
- 避免在每次調用方法時複製該值,在值的類型爲大型結構體時,這樣做會更加高效。