天天看點

GO 的方法集前言分析

前言

之前在寫

GO

demo 的時候, 寫了這麼一段程式(大概意思):

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()

, 調用方法時的隐式轉址蒙蔽了我的雙眼... 雖然這樣在使用的時候就不用特意區分變量類型是值還是位址, 但是有的地方幫我轉了, 有的地方又不管我了, 感覺怪怪的. 再習慣習慣.