前言
之前在写 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()
, 调用方法时的隐式转址蒙蔽了我的双眼… 虽然这样在使用的时候就不用特意区分变量类型是值还是地址, 但是有的地方帮我转了, 有的地方又不管我了, 感觉怪怪的. 再习惯习惯.