前言
之前在寫
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 的方法集):
- 指針類型擁有 值/指針 的方法
- 值類型隻擁有值類型的方法
那麼問題來了, 我平常寫的時候, 是這樣的, 就不會報錯呀, 怎麼今天突然報錯了? 他們有什麼差別麼?
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()
, 調用方法時的隐式轉址蒙蔽了我的雙眼... 雖然這樣在使用的時候就不用特意區分變量類型是值還是位址, 但是有的地方幫我轉了, 有的地方又不管我了, 感覺怪怪的. 再習慣習慣.