GO 的方法集

前言

之前在寫 GOdemo 的時候, 寫了這麼一段程序(大概意思):

package main

type Test struct {
}

func (test *Test) print()  {
	println("test fun")
}

func main() {
	Test{}.print()
}

結果一編譯就報錯了: cannot call pointer method on Test literal

差不多意思是不能調用指針方法. 我一看, 確實, print方法聲明的是指針類型. 這麼說我就懂了, 加個取址就 OK 了吧? (&Test{}).print() 這樣就可以調用了.

分析

由此大膽的假設, GO在將方法綁定到結構體的時候, 根據接收的結構體類型不同(值或指針), 會將方法綁定到不同的類型變量上, 也就是說, 指針類型只能調用指針類型的方法, 值類型只能調用值類型的方法.

驗證一下:

package main

type Test struct {
}

func (test *Test) print()  {
	println("test fun")
}

func (test Test) print2()  {
	println("test fun 2")
}

func main() {
	// 指針類型調用值類型方法
	(&Test{}).print2()
	// 指針類型調用指針類型方法
	(&Test{}).print()
	// 值類型調用值類型方法
	Test{}.print2()
	// 值類型調用指針類型方法
	Test{}.print()
}

結果如何? 只有在使用值類型調用指針類型方法時, 編譯會報錯, 其他情況都 OK.

假設推翻, GO方法的綁定規則應該是(網上搜了搜, 發現這玩意叫 GO 的方法集):

  1. 指針類型擁有 值/指針 的方法
  2. 值類型只擁有值類型的方法

那麼問題來了, 我平常寫的時候, 是這樣的, 就不會報錯呀, 怎麼今天突然報錯了? 他們有什麼區別麼?

t := Test{}
t.print()

我十分確定, t變量不是指針, 但他就可以調用呀. 查了查發現, 是GO在編譯的時候幫我們隱式的做了取址的操作. 那爲什麼這裏可以幫忙, 上面就不行了呢? 搞不懂.

在查的時候, 還看到了大概這樣的代碼:

package main

// 定義個測試接口
type ITest interface {
	print()
}

type Test struct {
}

// 實現接口的類
func (test *Test) print()  {
	println("test fun")
}

func main() {
	ReceiveTest(Test{})
}

// 接收接口的方法
func ReceiveTest(t ITest)  {
	t.print()
}

這個時候, 向方法傳值就會報錯, 有了上面的經驗, 我已經知道了, 值類型沒有綁定print方法, 所以改成傳遞指針就可以了.而且, 在這裏, 如果在 ReceiveTest方法中做取址的操作, 也麼的用, 只能在向方法傳參的時候做取值操作.

這裏再假設一下, 方法在傳參的時候是傳遞的複製值, 當對值進行復制傳進函數的時候, 儼然已經不是原始的值了, 而是原始值的一個副本, 而對副本再進行取址, 已經是一個新地址了, 自然就沒有綁定其指針函數. 而當參數是指針類型的時候, 對指針類型複製並傳遞, 方法接收到的是一個地址值, 雖然此地址值是一個副本, 但是指向的仍然是原對象.

OK, 驗證假設(爲了保證編譯順利, 只保留了基本內容):

package main

import "fmt"

type Test struct {
	Name int
}

func main() {
	t := Test{}
	fmt.Printf("%p\n", &t)
	ReceiveTest(t)
}

func ReceiveTest(t Test)  {
	fmt.Printf("%p\n", &t)
}

打印結果不同, 果然不是同一個對象, 而是複製的一個副本. 而對於指針傳遞:

package main

import "fmt"

type Test struct {
	Name int
}

func main() {
	t := &Test{}
	fmt.Printf("原始指針變量的地址: %p\n", &t)
	fmt.Printf("原始指針變量的值: %p\n", t)
	ReceiveTest(t)
}

// 接收接口的方法
func ReceiveTest(t *Test)  {
	fmt.Printf("接收指針變量的地址: %p\n", &t)
	fmt.Printf("接收指針變量的值: %p\n", t)
}

打印結果:

原始指針變量的地址: 0xc00000e028
原始指針變量的值: 0xc000016068
接收指針變量的地址: 0xc00000e038
接收指針變量的值: 0xc000016068

結果發現, 指針傳遞保存的對象地址確實會原封不動的傳遞, 但是, 其指針變量卻會創建副本傳進來. 所以可以這樣理解, 不管你是指針類型還是值類型, GO 在函數傳參的時候, 都會對該內容創建一個副本進行傳遞.

那也就意味着, 如果傳的是一個較大的對象, 進行值的傳遞, 會將整個對象全拷貝一份, 然後傳遞過去, 而傳遞指針只需要拷貝8字節的指針數據就可以了,

不過如果傳入了指針類型, 就要直面在方法內部可能會對對象進行修改的風險.


至此, 最開始的疑問已經解答了, 被GO這個t.print(), 調用方法時的隱式轉址矇蔽了我的雙眼... 雖然這樣在使用的時候就不用特意區分變量類型是值還是地址, 但是有的地方幫我轉了, 有的地方又不管我了, 感覺怪怪的. 再習慣習慣.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章