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


原文地址 https://hujingnb.com/archives/348

转载请保留原文连接: GO 的方法集 | 烟草的香味

订阅评论
提醒
guest
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请发表评论。x