1.深究
1.1两大问题
基于我们之前介绍过的类型元数据相关知识,T和*T是两种类型,分别有着自己的类型元数据,一些元数据相关知识t he兴替是两种类型,分别有着自己的类型元数据,而根据自定义类型的类型元数据可以找得到该类型关联的方法列表。
既然T和*T各有各的方法集,那为什么还要限制T和*T不能定义同名方法,
又怎么会有"*T的方法集包含T的方法集合“这种说法?
1.2T的方法集:接收者为T类型的方法
接下来咱们就来梳理下这两个问题。
首先可以确定的是,T的方法集里全部都是有明确定义的接收者为T类型的方法,
1.3T的方法集:接收者为T类型的方法+包装方法
而*T的方法集里除了有明确定义的接收者为*T的方法以外,还会有编译器生成的一些”包装方法“,这些包装方法是对接收者为T类型的同名方法的“包装”,
1.4包装方法语法糖
为什么编辑器要为接受者为T的方法包装一个接收者为*T的同名方法呢?
这里首先要明确一点,“通过*T类型的变量直接调用T类型接收者的方法”只是一种语法糖,经验证:这种调用方式,编译器会在调入端进行指针解引用,并不会用到这里的包装方法。
1.5解释两大问题
实际上编译器生成包装方法主要是为了支持接口,还记得接口的数据结构吗?。涉及到方法,自然是非空接口。它只包含两个指针,一个和类型元数据相关,一个和接口装载的数据相关。
虽然有数据指针,但却不能像这里的语法糖那样通过指针解引用来调用值接收者的方法。
至于原因嘛,你想,方法的接收者是方法调用时隐含的第一个参数,go语言中函数参数是通过栈来传递的,如果参数是指针类型,那就很好实现,平台确定了,指针大小就确定了。但如果要解引用为值类型,就要有明确的类型信息,编译器才能确定这个参数要在栈上占用多大的空间。
而对于接口,编译阶段并不能确定他会装载哪一类数据,所以编译器也不能生成对应的指令来解引用
总而言之,接口不能直接使用接收者为值类型的方法。
面对这个问题,编译器选择为值接收者的方法生成指针接收者的同名包装方法这一解决方案。也正因为如此,如果我们再给*T和T定义同名方法,就有可能和编译器生成的包装方法发生冲突,所以go语言干脆不允许为T和*T定义同名方法。
1.6分析可执行文件
至于说"*T的方法集"及包含"T的方法集的所有方法"。这种说法可以这样理解。虽然编译器会对所有接收者为T的方法生成接收者为*T的包装方法,但是链接器会把程序中确定不会用到的方法都裁剪掉
所以,如果去分析可执行文件的话,就会发现,不只是这些包装方法,就连我们明确定义的方法也不一定会存在于可执行文件中。不过一定要记得从可执行文件去分析,不能通过反射在程序中验证,因为反射的实现也是基于接口,通过反射来验证,会被链接器认为用到了这个方法,从而把它保留下来,这就测不准了。
2.附
2.1接口传值,调用值接收者,不改变原来的值
package main
import "fmt"
type Iface interface {
hello()
}
type Stu struct {
}
func (c Stu) hello() {
fmt.Println("11111")
}
func main() {
var IfTs Iface
var stu Stu
IfTs = stu
IfTs.hello() //"11111"
}
不会改变到的值
package main
import "fmt"
type Iface interface {
hello()
}
type Stu struct {
a int
}
func (c Stu) hello() {
c.a = 10000
}
func main() {
var IfTs Iface
var stu Stu = Stu{2}
IfTs = stu
IfTs.hello()
fmt.Println(stu) //{2}
}
2.2接口传指针,调用指针接收者,改变到原来的值
package main
import "fmt"
type Iface interface {
hello()
}
type Stu struct {
}
func (c *Stu) hello() {
fmt.Println("11111")
}
func main() {
var IfTs Iface
var stu Stu
IfTs = &stu
IfTs.hello()
}
改变到原来的值
package main
import "fmt"
type Iface interface {
hello()
}
type Stu struct {
a int
}
func (c *Stu) hello() {
c.a = 10000
}
func main() {
var IfTs Iface
var stu Stu = Stu{2}
IfTs = &stu
IfTs.hello()
fmt.Println(stu) //{10000}
}
2.3接口传指针,调用值接收者,不改变到原来的值(success)
package main
import "fmt"
type Iface interface {
hello()
}
type Stu struct {
}
func (c Stu) hello() {
fmt.Println("11111111")
}
func main() {
var IfTs Iface
var stu Stu
IfTs = &stu
IfTs.hello() //11111111
}
不改变到原来的值
package main
import "fmt"
type Iface interface {
hello()
}
type Stu struct {
a int
}
func (c Stu) hello() {
c.a = 10000
}
func main() {
var IfTs Iface
var stu Stu = Stu{2}
IfTs = &stu
IfTs.hello()
fmt.Println(stu) //{2}
}
2.4接口传值,调用指针接收者(error)
package main
type A interface {
hello()
}
type B struct {
}
func (c *B) hello() {
panic("implement me")
}
func main() {
var a A
var b B
a = b //在此编译报错
a.hello()
}
2.5附
1:
package main
import "fmt"
type T struct {
}
func (t T) A() {
}
func (pt *T) A() {
}
2:
package main
type I interface {
A()
}
func main() {
var i I
i.A() //是可以编译过的,但是运行不通过,引发panic
}
2.6总结
那为什么第三种情况可以正常运行呢?通过*T类型的变量调用T类型的接收者的方法,只是一种语法糖,编译器会在调用端进行指针解引用。 那为什么第四种情况会err呢?因为*T和T是两种不同的类型,这里没有语法糖,T类型没有实现接口的方法,自然会err咯。无法将’stu’ (类型Stu)用作类型Iface类型,未实现’Iface’
总结:
因为通过 *T 类型的变量调用T类型的接收者的方法,是一种语法糖,所以可以简单的理解为 *T 的方法集包括了所有的T方法集,这也就是为什么会有人说,只要接收者里面有一个是指针接收者,那就把所有的方法接收者都改为指针接收者的原因了。不过需要注意的是T和 *T 是两种不同的类型!