Go 学习笔记6-Go方法
目录
Go 语言中的方法和面向对象中的方法并不是一样的。Go 引入方法这一元素,并不是要支持面向对象编程范式,而是 Go 践行组合设计哲学的一种实现层面的需要。
Go 语言中的方法的本质就是,一个以方法的 receiver 参数作为第一个参数的普通函数。
1,Go 方法的定义
Go 方法的形式,比函数多了一个 receiver :
receiver 参数也是方法与类型之间的纽带,也是方法与函数的最大不同。
Go 中的方法必须是归属于一个类型的,而 receiver 参数的类型就是这个方法归属的类型。
注意 ListenAndServeTLS 是 *Server 类型的方法,而不是 Server 类型的方法。
方法的一般声明形式:
func (t *T或T) MethodName(参数列表) (返回值列表) {
// 方法体
}
- 无论 receiver 参数的类型为 *T 还是 T,我们都将 T 叫做 t 的基类型
- 如果 t 的类型为 T,那么说这个方法是类型 T 的一个方法
- 如果 t 的类型为 *T,那么就说这个方法是类型 *T 的一个方法
- receiver 参数的基类型本身不能为指针类型或接口类型
- 每个方法只能有一个 receiver 参数
- Go 不支持在方法的 receiver 部分放置包含多个 receiver 参数的参数列表,或者变长 receiver 参数
- 方法声明要与 receiver 参数的基类型声明放在同一个包内
- 我们无法为原生类型(诸如 int、float64、map 等)添加方法
- 不能跨越 Go 包为其他包的类型声明新方法
Go 方法的调用形式:
type T struct{}
func (t T) M(n int) {
}
func main() {
var t T
t.M(1) // 通过类型T的变量实例调用方法M
p := &T{}
p.M(2) // 通过类型*T的变量实例调用方法M
}
我们可以将方法作为右值,赋值给一个函数类型的变量,比如下:
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(a int) int {
t.a = a
return t.a
}
func main() {
var t T
f1 := (*T).Set // f1的类型,也是*T类型Set方法的类型:func (t *T, int)int
f2 := T.Get // f2的类型,也是T类型Get方法的类型:func(t T)int
fmt.Printf("the type of f1 is %T\n", f1) // the type of f1 is func(*main.T, int) int
fmt.Printf("the type of f2 is %T\n", f2) // the type of f2 is func(main.T) int
f1(&t, 3)
fmt.Println(f2(t)) // 3
}
2,receiver 参数的类型问题
来看下面例子中的两个 Go 方法,以及它们等价转换后的函数:
func (t T) M1() <=> F1(t T)
func (t *T) M2() <=> F2(t *T)
- M1 方法是 receiver 参数类型为 T 的一类方法的代表
- T 类型实例的 receiver 参数以值传递方式传递到 M1 方法体中
- 实际上是 T 类型实例的副本,M1 方法体中对副本的任何修改操作,都不会影响到原 T 类型实例
- M2 方法是 receiver 参数类型为 *T 的另一类
- *T 类型实例的 receiver 参数以值传递方式传递到 M2 方法体中
- 实际上是 T 类型实例的地址,M2 方法体通过该地址可以对原 T 类型实例进行任何修改操作
一个示例:
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
}
选择 receiver 参数类型的原则:
- 如果 Go 方法要把对 receiver 参数代表的类型实例的修改,反映到原类型实例上,那么我们应该选择 *T 作为 receiver 参数的类型。
- 如果我们不需要在方法中对类型实例进行修改呢?这个时候我们是为 receiver 参数选择 T 类型还是 *T 类型呢?
- 一般情况下,我们会为 receiver 参数选择 T 类型,这样可以减少外部修改类型实例内部状态的“渠道”
- 如果 receiver 参数类型的 size 较大,以值拷贝形式传入就会导致较大的性能开销,这时我们选择 *T 作为 receiver 类型可能更好些
- T 类型是否要实现某一接口
- 如果 T 类型需要实现某一接口的全部方法,那么我们就需要使用 T 作为 receiver 参数的类型来满足接口类型方法集合中的所有方法。
- 如果 T 类型不需要实现某一接口,那么我们就可以参考原则一和原则二来为 receiver 参数选择类型了。
无论是 T 类型实例,还是 *T 类型实例,都既可以调用 receiver 为 T 类型的方法,也可以调用 receiver 为 *T 类型的方法 。这都是 Go 编译器在背后做的转换,当 Go 发现类型不一致时,会自动转换。
示例如下:
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
}
3,一个思考题
第一个:
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print()
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print()
}
time.Sleep(3 * time.Second)
}
// 其结果是
one
two
three
six
six
six
第二个:
type field struct {
name string
}
func (p field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print()
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print()
}
time.Sleep(3 * time.Second)
}
// 其结果是
one
two
three
four
five
six
4,方法集合
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")
}
再看下面代码:
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
Go 语言规定,*T 类型的方法集合包含所有以 *T 为 receiver 参数类型的方法,以及所有以 T 为 receiver 参数类型的方法。
方法集合决定接口实现的含义就是:如果某类型 T 的方法集合与某接口类型的方法集合相同,或者类型 T 的方法集合是接口类型 I 方法集合的超集,那么我们就说这个类型 T 实现了接口 I。或者说,方法集合这个概念在 Go 语言中的主要用途,就是用来判断某个类型是否实现了某个接口。
... method has pointer receiver 问题
看代码:
type QuackableAnimal interface {
Quack()
}
type Duck struct{}
// d 的类型是 Duck
func (d Duck) Quack() {
}
func AnimalQuackInForest(a QuackableAnimal) {
}
func main() {
AnimalQuackInForest(Duck{}) // 传 T 类型没问题
AnimalQuackInForest(&Duck{}) // 传 *T 类型没问题
}
查看方法集:
dumpMethodSet(Duck{})
dumpMethodSet(&Duck{})
// 输出如下
main.Duck's method set:
- Quack
*main.Duck's method set:
- Quack
再看代码:
type QuackableAnimal interface {
Quack()
}
type Duck struct{}
// d 的类型是 *Duck
func (d *Duck) Quack() {
}
func AnimalQuackInForest(a QuackableAnimal) {
}
func main() {
AnimalQuackInForest(Duck{}) // 传 T 类型有问题,编译异常:Quack method has pointer receiver
AnimalQuackInForest(&Duck{}) // 传 *T 类型没问题
}
此时 AnimalQuackInForest(Duck{})
出问题的原因是,Duck 类型没有实现 QuackableAnimal 接口。
通过 dumpMethodSet(Duck{})
查看 Duck 的方法集,可知 Duck 的方法集为空。
dumpMethodSet(Duck{})
dumpMethodSet(&Duck{})
// 输出如下
main.Duck's method set is empty!
*main.Duck's method set:
- Quack
(完。)
文章作者 @码农加油站
上次更改 2022-06-11