golang的值接收者和指針接收者的區別

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 

從打印出的地址可以看出:

  1. 對於值接收者,如果調用者也是值對象,那麼會將調用者的值拷貝一份,並執行方法,方法的調用不會影響到調用者值。 如果調用者是指針對象,那麼會解引用指針對象爲值,然後將解引的對象拷貝一份,然後 執行方法。
  2. 對於指針接收者,如果調用者是值對象,會使用值的引用來調用方法,上例中,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。

有一個簡單的解釋:接收者是指針類型的方法,很可能在方法中會對接收者的屬性進行更改操作,從而影響接收者;而對於接收者是值類型的方法,在方法中不會對接收者本身產生影響。所以,當實現了一個接收者是值類型的方法,就可以自動生成一個接收者是對應指針類型的方法,因爲兩者都不會影響接收者。但是,當實現了一個接收者是指針類型的方法,如果此時自動生成一個接收者是值類型的方法,原本期望對接收者的改變(通過指針實現),現在無法實現,因爲值類型會產生一個拷貝,不會真正影響調用者。

結論就是:如果實現了接收者是值類型的方法,會隱含地也實現了接收者是指針類型的方法。

兩者分別在何時使用

如果方法的接收者是值類型,無論調用者是對象還是對象指針,修改的都是對象的副本,不影響調用者;如果方法的接收者是指針類型,則調用者修改的是指針指向的對象本身。

使用指針作爲方法的接收者的理由:

  • 方法能夠修改接收者指向的值。
  • 避免在每次調用方法時複製該值,在值的類型爲大型結構體時,這樣做會更加高效。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章