前言
之前在寫 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()
, 調用方法時的隱式轉址矇蔽了我的雙眼... 雖然這樣在使用的時候就不用特意區分變量類型是值還是地址, 但是有的地方幫我轉了, 有的地方又不管我了, 感覺怪怪的. 再習慣習慣.