【golang】非指针/指针接收器、包装方法、自动取/解引用

984 阅读5分钟

T和*T类型

我们知道方法接收器可以是T类型,也可以是*T类型,前者称为非指针接收器,后者称为指针接收器

T*T本质是两种不同数据类型

包装方法

看一个现象

示例程序:

type I1 interface {
   Method1() string
}

type S1 struct {
}

// 非指针接收器。即S1类型实现I1接口
func (s S1) Method1() string {
   return "S1"
}

type S2 struct {
}

// 指针接收器。即*S2类型实现接口
func (s *S2) Method1() string {
   return "S2"
}

func main() {
   s1 := S1{}
   s1Pt := &s1
   test(s1) // S1是I1接口类型
   test(s1Pt) // *S1也是I1接口类型

   s2 := S2{}
   s2Pt := &s2
   test(s2) // 编译出错:S2类型没有实现接口
   test(s2Pt) // *S2是I1接口类型
}

func test(i1 I1) {
   fmt.Println(i1.Method1())
}

可以看到:
S1使用的是非指针接收器实现接口方法,但是S1*S1都是I1接口类型,而S2使用的是指针接收器实现接口方法,只有*S2才是I1接口类型,而S2不是I1接口类型。

为什么会这样?

  1. 先说为什么*S2I1接口类型,而S2却不是I1接口类型?
    答:因为方法接收器是指针类型,*S2类型才实现了接口,而S2类型并没有实现接口,*S2S2本质是两种类型。
  2. 明明只有S1类型实现了接口,而*S1为啥也是接口类型?
    答:因为「包装方法」。

什么是包装方法

是编译器提供的一个语法糖
编译器会为方法接收器为非指针类型的方法自动生成一个方法接收器是指针类型的方法,这就是包装方法。

从「包装方法」的角度去分析,就能解释为啥上述程序明明只有S1类型实现了I1接口,而*S1类型却也是I1接口类型:就是因为编译器自动生成了包装方法,使*S1类型也实现了I1接口。

为什么编译器要生成「包装方法」

先说结论:

为了支持接口类型调用非指针接收者方法


再分析:

我们知道接口的数据结构如下:

type iface struct {
  tab  *itab
  data unsafe.Pointer
}

我们可能会用接口类型去调用指针接收者方法,也可能调用非指针接收者方法,这取决于类型如何实现接口:是使用T还是*T类型来实现。

  • 通过接口类型调用指针接收者方法时,传递地址是非常方便的,也不用关心数据的具体类型,地址的大小总是一致的。

  • 通过接口类型调用非指针接收者方法时,就需要拷贝数据值,但是由于编译阶段不能确定接口的具体类型,所以编译器不能生成相关的指令来完成拷贝,为了解决这个问题,于是编译器会生成包装方法,生成包装方法后,指针类型也实现了接口,此时就可以拷贝指针值了,编译器生成拷贝指针值的指令即可,以此实现接口类型调用非指针接收者方法。这就是编译器生成包装方法的根本原因

猜测具体原理(待验证):生成的包装方法内部会委托调用非指针接收者方法,以此实现接口类型调用非指针接收者方法。

验证猜测的程序:

type SetAge interface {
   setAge(age int)
}

type Person struct {
   Name string
   Age  int
}

// 非指针接收者方法。会自动生成一个包装方法
func (p Person) setAge(age int) {
   p.Age = age
}

func main() {
   person := Person{"PENG", 18}
   person.setAge(20)
   fmt.Printf("person:%+v\n", person) // 输出:person:{Name:PENG Age:18}

   var i SetAge = person // 接口值是Person结构体
   i.setAge(22)         // 接口类型调用非指针接收者方法,编译时,会拷贝地址值
   fmt.Printf("person:%+v\n", person) // 输出:person:{Name:PENG Age:18},结构体不变

   var i2 SetAge = &person            // 接口值是指针
   i2.setAge(24)                      // 接口类型调用非指针接收者方法,编译时,会拷贝地址值
   fmt.Printf("person:%+v\n", person) // 输出:person:{Name:PENG Age:18},结构体不变
}

可以看到:person变量的值一直不变
猜测生成的包装方法:

// 生成的包装方法应该不是这样,否则会改变入参结构体的值
func (p *Person) setAge(age int) {
   p.Age = age
}

// 生成的包装方法应该是这样:内部委托调用非指针接收者方法,不会改变入参结构体的值
func (p *Person) setAge(age int) {
   (*p).setAge(age)
}

总结

包装方法是为了支持接口类型调用非指针接收者方法,包装方法使*T也实现了接口类型,然后通过接口调用方法时,只需要拷贝指针值即可,编译器只需要生成拷贝指针值的指令就行;包装方法内部会委托调用非指针接收者方法,从而实现接口类型调用非指针接收者方法。

自动取引用和自动解引用

是编译器提供的一个语法糖

  1. 使用指针类型调用非指针接收者方法,编译时会自动解引用
  2. 使用非指针类型调用指针接收者方法,编译时会自动取引用

示例程序如下:

type S1 struct {
}

func (s S1) test() {
   fmt.Println("S1 test")
}

type S2 struct {
}

func (s *S2) test() {
   fmt.Println("*S2 test")
}

func main() {
   s1 := &S1{}
   s1.test() // 使用指针调用非指针接收者方法,会自动解引用

   s2 := S2{}
   s2.test() // 使用值调用指针接收者方法,会自动取引用
}

总结

  1. T*T本质是两种不同类型
  2. 由于「包装方法」的存在,T类型实现了某接口,则*T类也实现了该接口;但*T类型实现了某接口,T类型却并没有实现该接口
  3. 包装方法是为了支持接口类型调用非指针接收者方法,包装方法使*T也实现了接口类型,然后通过接口调用方法时,只需要拷贝指针值即可,编译器只需要生成拷贝指针值的指令就行;包装方法内部会委托调用非指针接收者方法,从而实现接口类型调用非指针接收者方法。
  4. 调用方法时,若T*T类型不匹配,编译器会自动解引用或取引用。

参考

zhuanlan.zhihu.com/p/374592333

blog.csdn.net/weixin_5269…