极客时间已完结课程限时免费阅读

25|方法:方法集合与如何选择receiver类型?

25|方法:方法集合与如何选择receiver类型?-极客时间

25|方法:方法集合与如何选择receiver类型?

讲述:Tony Bai

时长16:00大小14.62M

你好,我是 Tony Bai。
在上一节中我们开启了 Go 方法的学习,了解了 Go 语言中方法的组成、声明和实质。可以说,我们已经正式入门 Go 方法了。
入门 Go 方法后,和函数一样,我们要考虑如何进行方法设计的问题。由于在 Go 语言中,方法本质上就是函数,所以我们之前讲解的、关于函数设计的内容对方法也同样适用,比如错误处理设计、针对异常的处理策略、使用 defer 提升简洁性,等等。
但关于 Go 方法中独有的 receiver 组成部分,却没有现成的、可供我们参考的内容。而据我了解,初学者在学习 Go 方法时,最头疼的一个问题恰恰就是如何选择 receiver 参数的类型
那么,在这一讲中,我们就来学习一下不同 receiver 参数类型对 Go 方法的影响,以及我们选择 receiver 参数类型时的一些经验原则。

receiver 参数类型对 Go 方法的影响

要想为 receiver 参数选出合理的类型,我们先要了解不同的 receiver 参数类型会对 Go 方法产生怎样的影响。在上一节课中,我们分析了 Go 方法的本质,得出了“Go 方法实质上是以方法的 receiver 参数作为第一个参数的普通函数”的结论。
对于函数参数类型对函数的影响,我们是很熟悉的。那么我们能不能将方法等价转换为对应的函数,再通过分析 receiver 参数类型对函数的影响,从而间接得出它对 Go 方法的影响呢?
我们可以基于这个思路试试看。我们直接来看下面例子中的两个 Go 方法,以及它们等价转换后的函数:
func (t T) M1() <=> F1(t T)
func (t *T) M2() <=> F2(t *T)
这个例子中有方法 M1 和 M2。M1 方法是 receiver 参数类型为 T 的一类方法的代表,而 M2 方法则代表了 receiver 参数类型为 *T 的另一类。下面我们分别来看看不同的 receiver 参数类型对 M1 和 M2 的影响。
首先,当 receiver 参数的类型为 T 时:
当我们选择以 T 作为 receiver 参数类型时,M1 方法等价转换为F1(t T)。我们知道,Go 函数的参数采用的是值拷贝传递,也就是说,F1 函数体中的 t 是 T 类型实例的一个副本。这样,我们在 F1 函数的实现中对参数 t 做任何修改,都只会影响副本,而不会影响到原 T 类型实例。
据此我们可以得出结论:当我们的方法 M1 采用类型为 T 的 receiver 参数时,代表 T 类型实例的 receiver 参数以值传递方式传递到 M1 方法体中的,实际上是 T 类型实例的副本,M1 方法体中对副本的任何修改操作,都不会影响到原 T 类型实例。
第二,当 receiver 参数的类型为 *T 时:
当我们选择以 *T 作为 receiver 参数类型时,M2 方法等价转换为F2(t *T)。同上面分析,我们传递给 F2 函数的 t 是 T 类型实例的地址,这样 F2 函数体中对参数 t 做的任何修改,都会反映到原 T 类型实例上。
据此我们也可以得出结论:当我们的方法 M2 采用类型为 *T 的 receiver 参数时,代表 *T 类型实例的 receiver 参数以值传递方式传递到 M2 方法体中的,实际上是 T 类型实例的地址,M2 方法体通过该地址可以对原 T 类型实例进行任何修改操作。
我们再通过一个更直观的例子,证明一下上面这个分析结果,看一下 Go 方法选择不同的 receiver 类型对原类型实例的影响:
package main
type T struct {
a int
}
func (t T) M1() {
t.a = 10
}
func (t *T) M2() {
t.a = 11
}
func main() {
var t T
println(t.a) // 0
t.M1()
println(t.a) // 0
p := &t
p.M2()
println(t.a) // 11
}
在这个示例中,我们为基类型 T 定义了两个方法 M1 和 M2,其中 M1 的 receiver 参数类型为 T,而 M2 的 receiver 参数类型为 *T。M1 和 M2 方法体都通过 receiver 参数 t 对 t 的字段 a 进行了修改。
但运行这个示例程序后,我们看到,方法 M1 由于使用了 T 作为 receiver 参数类型,它在方法体中修改的仅仅是 T 类型实例 t 的副本,原实例并没有受到影响。因此 M1 调用后,输出 t.a 的值仍为 0。
而方法 M2 呢,由于使用了 *T 作为 receiver 参数类型,它在方法体中通过 t 修改的是实例本身,因此 M2 调用后,t.a 的值变为了 11,这些输出结果与我们前面的分析是一致的。
了解了不同类型的 receiver 参数对 Go 方法的影响后,我们就可以总结一下,日常编码中选择 receiver 的参数类型的时候,我们可以参考哪些原则。

选择 receiver 参数类型的第一个原则

基于上面的影响分析,我们可以得到选择 receiver 参数类型的第一个原则:如果 Go 方法要把对 receiver 参数代表的类型实例的修改,反映到原类型实例上,那么我们应该选择 *T 作为 receiver 参数的类型
这个原则似乎很好掌握,不过这个时候,你可能会有个疑问:如果我们选择了 *T 作为 Go 方法 receiver 参数的类型,那么我们是不是只能通过 *T 类型变量调用该方法,而不能通过 T 类型变量调用了呢?这个问题恰恰也是上节课我们遗留的一个问题。我们改造一下上面例子看一下:
type T struct {
a int
}
func (t T) M1() {
t.a = 10
}
func (t *T) M2() {
t.a = 11
}
func main() {
var t1 T
println(t1.a) // 0
t1.M1()
println(t1.a) // 0
t1.M2()
println(t1.a) // 11
var t2 = &T{}
println(t2.a) // 0
t2.M1()
println(t2.a) // 0
t2.M2()
println(t2.a) // 11
}
我们先来看看类型为 T 的实例 t1。我们看到它不仅可以调用 receiver 参数类型为 T 的方法 M1,它还可以直接调用 receiver 参数类型为 *T 的方法 M2,并且调用完 M2 方法后,t1.a 的值被修改为 11 了。
其实,T 类型的实例 t1 之所以可以调用 receiver 参数类型为 *T 的方法 M2,都是 Go 编译器在背后自动进行转换的结果。或者说,t1.M2() 这种用法是 Go 提供的“语法糖”:Go 判断 t1 的类型为 T,也就是与方法 M2 的 receiver 参数类型 *T 不一致后,会自动将t1.M2()转换为(&t1).M2()
同理,类型为 *T 的实例 t2,它不仅可以调用 receiver 参数类型为 *T 的方法 M2,还可以调用 receiver 参数类型为 T 的方法 M1,这同样是因为 Go 编译器在背后做了转换。也就是,Go 判断 t2 的类型为 *T,与方法 M1 的 receiver 参数类型 T 不一致,就会自动将t2.M1()转换为(*t2).M1()
通过这个实例,我们知道了这样一个结论:无论是 T 类型实例,还是 *T 类型实例,都既可以调用 receiver 为 T 类型的方法,也可以调用 receiver 为 *T 类型的方法。这样,我们在为方法选择 receiver 参数的类型的时候,就不需要担心这个方法不能被与 receiver 参数类型不一致的类型实例调用了。

选择 receiver 参数类型的第二个原则

前面我们第一个原则说的是,当我们要在方法中对 receiver 参数代表的类型实例进行修改,那我们要为 receiver 参数选择 *T 类型,但是如果我们不需要在方法中对类型实例进行修改呢?这个时候我们是为 receiver 参数选择 T 类型还是 *T 类型呢?
这也得分情况。一般情况下,我们通常会为 receiver 参数选择 T 类型,因为这样可以缩窄外部修改类型实例内部状态的“接触面”,也就是尽量少暴露可以修改类型内部状态的方法。
不过也有一个例外需要你特别注意。考虑到 Go 方法调用时,receiver 参数是以值拷贝的形式传入方法中的。那么,如果 receiver 参数类型的 size 较大,以值拷贝形式传入就会导致较大的性能开销,这时我们选择 *T 作为 receiver 类型可能更好些。
以上这些可以作为我们选择 receiver 参数类型的第二个原则
到这里,你可能会发出感慨:即便有两个原则,这似乎依旧很容易掌握!不要大意,这可没那么简单,这两条只是基础原则,还有一条更难理解的原则在下面呢。
不过在讲解这第三条原则之前,我们先要了解一个基本概念:方法集合(Method Set),它是我们理解第三条原则的前提。

方法集合

在了解方法集合是什么之前,我们先通过一个示例,直观了解一下为什么要有方法集合,它主要用来解决什么问题:
type Interface interface {
M1()
M2()
}
type T struct{}
func (t T) M1() {}
func (t *T) M2() {}
func main() {
var t T
var pt *T
var i Interface
i = pt
i = t // cannot use t (type T) as type Interface in assignment: T does not implement Interface (M2 method has pointer receiver)
}
在这个例子中,我们定义了一个接口类型 Interface 以及一个自定义类型 T。Interface 接口类型包含了两个方法 M1 和 M2,代码中还定义了基类型为 T 的两个方法 M1 和 M2,但它们的 receiver 参数类型不同,一个为 T,另一个为 *T。在 main 函数中,我们分别将 T 类型实例 t 和 *T 类型实例 pt 赋值给 Interface 类型变量 i。
运行一下这个示例程序,我们在i = t这一行会得到 Go 编译器的错误提示,Go 编译器提示我们:T 没有实现 Interface 类型方法列表中的 M2,因此类型 T 的实例 t 不能赋值给 Interface 变量
可是,为什么呀?为什么 *T 类型的 pt 可以被正常赋值给 Interface 类型变量 i,而 T 类型的 t 就不行呢?如果说 T 类型是因为只实现了 M1 方法,未实现 M2 方法而不满足 Interface 类型的要求,那么 *T 类型也只是实现了 M2 方法,并没有实现 M1 方法啊?
有些事情并不是表面看起来这个样子的。了解方法集合后,这个问题就迎刃而解了。同时,方法集合也是用来判断一个类型是否实现了某接口类型的唯一手段,可以说,“方法集合决定了接口实现”。更具体的分析,我们等会儿再讲。
那么,什么是类型的方法集合呢?
Go 中任何一个类型都有属于自己的方法集合,或者说方法集合是 Go 类型的一个“属性”。但不是所有类型都有自己的方法呀,比如 int 类型就没有。所以,对于没有定义方法的 Go 类型,我们称其拥有空方法集合。
接口类型相对特殊,它只会列出代表接口的方法列表,不会具体定义某个方法,它的方法集合就是它的方法列表中的所有方法,我们可以一目了然地看到。因此,我们下面重点讲解的是非接口类型的方法集合。
为了方便查看一个非接口类型的方法集合,我这里提供了一个函数 dumpMethodSet,用于输出一个非接口类型的方法集合:
func dumpMethodSet(i interface{}) {
dynTyp := reflect.TypeOf(i)
if dynTyp == nil {
fmt.Printf("there is no dynamic type\n")
return
}
n := dynTyp.NumMethod()
if n == 0 {
fmt.Printf("%s's method set is empty!\n", dynTyp)
return
}
fmt.Printf("%s's method set:\n", dynTyp)
for j := 0; j < n; j++ {
fmt.Println("-", dynTyp.Method(j).Name)
}
fmt.Printf("\n")
}
下面我们利用这个函数,试着输出一下 Go 原生类型以及自定义类型的方法集合,看下面代码:
type T struct{}
func (T) M1() {}
func (T) M2() {}
func (*T) M3() {}
func (*T) M4() {}
func main() {
var n int
dumpMethodSet(n)
dumpMethodSet(&n)
var t T
dumpMethodSet(t)
dumpMethodSet(&t)
}
运行这段代码,我们得到如下结果:
int's method set is empty!
*int's method set is empty!
main.T's method set:
- M1
- M2
*main.T's method set:
- M1
- M2
- M3
- M4
我们看到以 int、*int 为代表的 Go 原生类型由于没有定义方法,所以它们的方法集合都是空的。自定义类型 T 定义了方法 M1 和 M2,因此它的方法集合包含了 M1 和 M2,也符合我们预期。但 *T 的方法集合中除了预期的 M3 和 M4 之外,居然还包含了类型 T 的方法 M1 和 M2!
不过,这里程序的输出并没有错误。
这是因为,Go 语言规定,*T 类型的方法集合包含所有以 *T 为 receiver 参数类型的方法,以及所有以 T 为 receiver 参数类型的方法。这就是这个示例中为何 *T 类型的方法集合包含四个方法的原因。
这个时候,你是不是也找到了前面那个示例中为何i = pt没有报编译错误的原因了呢?我们同样可以使用 dumpMethodSet 工具函数,输出一下那个例子中 pt 与 t 各自所属类型的方法集合:
type Interface interface {
M1()
M2()
}
type T struct{}
func (t T) M1() {}
func (t *T) M2() {}
func main() {
var t T
var pt *T
dumpMethodSet(t)
dumpMethodSet(pt)
}
运行上述代码,我们得到如下结果:
main.T's method set:
- M1
*main.T's method set:
- M1
- M2
通过这个输出结果,我们可以一目了然地看到 T、*T 各自的方法集合。
我们看到,T 类型的方法集合中只包含 M1,没有 Interface 类型方法集合中的 M2 方法,这就是 Go 编译器认为变量 t 不能赋值给 Interface 类型变量的原因。
在输出的结果中,我们还看到 *T 类型的方法集合除了包含它自身定义的 M2 方法外,还包含了 T 类型定义的 M1 方法,*T 的方法集合与 Interface 接口类型的方法集合是一样的,因此 pt 可以被赋值给 Interface 接口类型的变量 i。
到这里,我们已经知道了所谓的方法集合决定接口实现的含义就是:如果某类型 T 的方法集合与某接口类型的方法集合相同,或者类型 T 的方法集合是接口类型 I 方法集合的超集,那么我们就说这个类型 T 实现了接口 I。或者说,方法集合这个概念在 Go 语言中的主要用途,就是用来判断某个类型是否实现了某个接口。
有了方法集合的概念做铺垫,选择 receiver 参数类型的第三个原则已经呼之欲出了,下面我们就来看看这条原则的具体内容。

选择 receiver 参数类型的第三个原则

理解了方法集合后,我们再理解第三个原则的内容就不难了。这个原则的选择依据就是 T 类型是否需要实现某个接口,也就是是否存在将 T 类型的变量赋值给某接口类型变量的情况。
如果 T 类型需要实现某个接口,那我们就要使用 T 作为 receiver 参数的类型,来满足接口类型方法集合中的所有方法。
如果 T 不需要实现某一接口,但 *T 需要实现该接口,那么根据方法集合概念,*T 的方法集合是包含 T 的方法集合的,这样我们在确定 Go 方法的 receiver 的类型时,参考原则一和原则二就可以了。
如果说前面的两个原则更多聚焦于类型内部,从单个方法的实现层面考虑,那么这第三个原则则是更多从全局的设计层面考虑,聚焦于这个类型与接口类型间的耦合关系。

小结

好了,今天的课讲到这里就结束了,现在我们一起来回顾一下吧。
我们前面已经知道,Go 方法本质上也是函数。所以 Go 方法设计的多数地方,都可以借鉴函数设计的相关内容。唯独 Go 方法的 receiver 部分,我们是没有现成经验可循的。这一讲中,我们主要学习的就是如何为 Go 方法的 receiver 参数选择类型。
我们先了解了不同类型的 receiver 参数对 Go 方法行为的影响,这是我们进行 receiver 参数选型的前提。
在这个前提下,我们提出了 receiver 参数选型的三个经验原则,虽然课程中我们是按原则一到三的顺序讲解的,但实际进行 Go 方法设计时,我们首先应该考虑的是原则三,即 T 类型是否要实现某一接口。
如果 T 类型需要实现某一接口的全部方法,那么我们就需要使用 T 作为 receiver 参数的类型来满足接口类型方法集合中的所有方法。
如果 T 类型不需要实现某一接口,那么我们就可以参考原则一和原则二来为 receiver 参数选择类型了。也就是,如果 Go 方法要把对 receiver 参数所代表的类型实例的修改反映到原类型实例上,那么我们应该选择 *T 作为 receiver 参数的类型。否则通常我们会为 receiver 参数选择 T 类型,这样可以减少外部修改类型实例内部状态的“渠道”。除非 receiver 参数类型的 size 较大,考虑到传值的较大性能开销,选择 *T 作为 receiver 类型可能更适合。
在理解原则三时,我们还介绍了 Go 语言中的一个重要概念:方法集合。它在 Go 语言中的主要用途就是判断某个类型是否实现了某个接口。方法集合像“胶水”一样,将自定义类型与接口隐式地“粘结”在一起,我们后面理解带有类型嵌入的类型时还会借助这个概念。

思考题

方法集合是一个很重要也很实用的概念,我们在下一节课还会用到这个概念帮助我们理解具体的问题。所以这里,我给你出了一道与方法集合有关的预习题。
如果一个类型 T 包含两个方法 M1 和 M2:
type T struct{}
func (T) M1()
func (T) M2()
然后,我们再使用类型定义语法,又基于类型 T 定义了一个新类型 S:
type S T
那么,这个 S 类型包含哪些方法呢?*S 类型又包含哪些方法呢?请你自己分析一下,然后借助 dumpMethodSet 函数来验证一下你的结论。
欢迎你把这节课分享给更多对 Go 方法感兴趣的朋友。我是 Tony Bai,我们下节课见。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 29

提建议

上一篇
24|方法:理解“方法”的本质
下一篇
26|方法:如何用类型嵌入模拟实现“继承”?
unpreview
 写留言

精选留言(24)

  • Geek_99b47c
    2021-12-10
    S 类型 和 *S 类型都没有包含方法,因为type S T 定义了一个新类型。 但是如果用 type S = T 则S和*S类型都包含两个方法。

    作者回复: 👍,抢答正确:)

    共 3 条评论
    57
  • 罗杰
    2021-12-10
    其实相比 Rust,Go 的糖更少,而且时而多,时而少,让开发者会很困惑,甚至前后矛盾。*T 和 T调用方法时编译器互相转换,哇,真贴心,真舒服。但是方法集合,又被 Go 反手打了一巴掌。的确解决了 C 语言的诸多问题,但对比 Rust 的一些处理方案,的确会让人不爽。

    作者回复: 是有点这种感觉哈。

    共 7 条评论
    20
  • liaomars
    2021-12-10
    老师: 如果 T 类型需要实现某个接口,那我们就要使用 T 作为 receiver 参数的类型,来满足接口类型方法集合中的所有方法。 这段描述感觉不对,根据上面举的例子来说,应该是使用 *T 作为 receiver参数的类型,来满足接口类型方法集合中的所有方法。

    作者回复: 我再举例说一下“如果 T 类型需要实现某个接口” 这句的含义。现在有一个接口类型I,一个自定义非接口类型T,这句的含义就是 我们希望 var i I var t T i = t 这段代码是ok的。即t可以赋值给i。 如果是*T实现了I,那么不能保证T也会实现I。所以我们在设计一个自定义类型T的方法时,考虑是否T需要实现某个接口。如果需要,方法receiver参数的类型应该是T。如果T不需要,那么用*T或T就都可以了。

    共 5 条评论
    11
  • 一步
    2021-12-11
    对于 类型 T 能不能 使用 *T 的方式,取决于 T 类型是不是可寻址的,在方法集合中也体现出来了,默认 T 类型是不包含 *T 的方法的
    共 1 条评论
    4
  • 左耳朵东
    2021-12-11
    如果因为 T 类型需要实现某一接口而使用 T 作为 receiver 参数的类型,那如果我想把在方法里对 t 的修改反映到原 T 类型实例上,何做到呢?

    作者回复: 其实这里的“T类型是否要实现接口”的含义是是否存在将T类型值赋值给接口类型的情况。如果存在,则必须用T作为receiver,如果不存在,按原则1和2。我让编辑更新了原文,增加了说明。希望增加后大家理解起来更容易些。

    共 5 条评论
    3
  • Calvin
    2021-12-11
    老师,dumpMethodSet 函数只能统计导出方法的,有没有办法把非导出方法的也统计出来?

    作者回复: 能不能得到非导出方法的信息取决于reflect包是否提供对应的能力,要不留个作业:探索一下reflect包,看是否能得到非导出方法的列表。:)

    共 2 条评论
    3
  • 进化菌
    2021-12-10
    *T 会把修改反应到原类型实例;*T 会对性能开销有关系;T 和 *T 的方法会隐式转换;实际进行 Go 方法设计时,我们首先应该考虑的是原则三,即 T 类型是否要实现某一接口,如要实现某一接口,使用 T 作为 receiver 参数的类型,来满足接口类型方法集合中的所有方法。
    3
  • 在下宝龙、
    2021-12-10
    S类型和*S类型都是 空方法,因为S是新的类型,它不能调用T的方法,必须显示转换之后才可以调用,所以本身的S或*S类型都不具有任何的方法

    作者回复: 👍

    3
  • lesserror
    2021-12-11
    追更老师的文章到现在,解答了我之前很多的困惑。也发现了一些新问题,希望老师抽空解答一下。 1. var t1 T t2 := &t 和 var t2 = &T{} ,这两种对结构体 T 的实例化方式有什么区别呢? 我从别的语言转到Go,就是很多时候被Go的一些奇奇怪怪的写法绕晕了。 2. & 和 * 能不能 单独好好讲讲,看Go的代码,都是满屏的 & 和 * ,对于动态语言的人来说,真的很难适应。 3. 文中的 NumMethod 方法,我点开方法的源码处的注释部分,这么写:“Note that NumMethod counts unexported methods only for interface types.” 这里的 unexported 代表的是未导出的意思,应该统计的是未导出的方法,但是我看文中统计了 导出方法的个数,感觉不理解。 4. 文中说:“Interface 接口类型包含了两个方法 M1 和 M2,它们的基类型都是 T。 ” 我想的是这句话表述有问题的,仅仅才接口的方法列表中,是看不出它们的基类型的呀。
    展开

    作者回复: 问题1和问题2: go弱化了指针的作用,因此大纲里没有专门讲指针,看来必须用一篇加餐把指针好好说一下了。 问题3:那句英文是一个提醒。提醒的是NumMethod对于interface类型来说,其统计的方法数量包含非导出方法。 问题4: 的确是表述不精确的问题,我让编辑老师改一下。

    共 5 条评论
    1
  • Calvin
    2021-12-11
    老师,go 方法可以“多实现”(“多继承”)吗?

    作者回复: 嵌入多个不同类型呗,你要说的“实现”是这样的么:)

    共 4 条评论
    1
  • 哈哈哈哈哈
    2021-12-10
    var t2 = &T{} 中&是什么意思?

    作者回复: &是取地址操作符。这样t2这个变量的实际类型为*T,即T类型的指针类型。

    共 7 条评论
    1
  • Geek_0b92d9
    2021-12-10
    都是空方法集合。并没有定义 receiver 为 S 或者 &S 的方法

    作者回复: 👍

    1
  • 叶鑫
    2021-12-10
    真是太棒了

    作者回复: :)

    1
  • 无双
    2021-12-10
    可以讲一下go的指针吗?

    作者回复: go语言弱化了指针的作用。至少指针没有在c中地位那么高。所以大纲没有安排,后续看是否用加餐补充吧。

    共 2 条评论
    1
  • 靠近我,温暖你
    2023-01-29 来自河南
    S 类型 和 *S 类型都没有包含方法,因为type S T 定义了一个新类型。 但是如果用 type S = T 则S和*S类型都包含两个方法。

    作者回复: 👍

  • 夏天
    2022-11-21 来自北京
    方法接收者类型选择三个原则 1.如果需要修改接收者本身,传指针 *T 2.如果接受者本身较为复杂,传指针 *T,避免拷贝 3.*T 的方法集合是包含 T 的方法集合。*T 范围更大 go 文档不推荐混合使用,一般还是用 T* 吧。除非明确需要不改动 T 本身
    展开

    作者回复: 👍

  • 菠萝吹雪—Code
    2022-08-19 来自北京
    思考题回答:type S T 相当于定义了一个新的类型,和T是完全不同的类型,测试结果,main.S's method set is empty! *main.S's method set is empty!

    作者回复: ✅

  • Return12321
    2022-07-16
    func main() { type S T var s1 S tool.DumpMethodSet(s1) tool.DumpMethodSet(&s1) type L = T var l1 L tool.DumpMethodSet(l1) tool.DumpMethodSet(&l1) } output: main.S's method set is empty *main.S's method set is empty main.T's method set: - M1 - M2 *main.T's method set: - M1 - M2 - M3 - M4 type S T 定义了一个新类型
    展开

    作者回复: 👍

  • mikewoo
    2022-04-28
    S's method set is empty! *S's method set is empty! 我的理解是type S T是定义了一个新类型。

    作者回复: 是的。

  • 白辉
    2022-02-09
    老师,您好,根据本节课内容有如下两个结论,那么T类型的实例可以调用receiver 为 *T 类型的方法,不能说明T类型的方法集合包含*T类型的方法吗? 1 通过这个实例,我们知道了这样一个结论:无论是 T 类型实例,还是 *T 类型实例,都既可以调用 receiver 为 T 类型的方法,也可以调用 receiver 为 *T 类型的方法。 2 Go 语言规定,*T 类型的方法集合包含所有以 *T 为 receiver 参数类型的方法,以及所有以 T 为 receiver 参数类型的方法。
    展开

    作者回复: T类型的实例可以调用receiver 为 *T 类型的方法 => 这个是go语法糖,不要与方法集合的概念弄混。方法集合更多用于决定是否实现了某个接口。