方法的设计
# 3.方法的设计
由于在 Go 语言中,方法本质上就是函数,所以关于函数设计的内容对方法也同样适用,比如错误处理设计、针对异常的处理策略、使用 defer 提升简洁性等等,只需要考虑Go 方法中独有的 receiver 组成部分。
# 3.1 选择receiver 参数的类型
不同receiver 参数类型对 Go 方法的影响
func (t T) M1() <=> F1(t T)
func (t *T) M2() <=> F2(t *T)
2
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
基类型 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 参数代表的类型实例的修改,反映到原类型实例上,那么我们应该选择 *T 作为 receiver 参数的类型。
- 一般情况下,通常会为 receiver 参数选择 T 类型,这样可以缩窄外部修改类型实例内部状态的“接触面”,尽量少暴露可以修改类型内部状态的方法。
- 特殊情况,考虑到 Go 方法调用时,receiver 参数是以值拷贝的形式传入方法中的。如果 receiver 参数类型的 size 较大,以值拷贝形式传入就会导致较大的性能开销,这时选择 *T 作为 receiver 类型可能更好些。
注意:无论选择的Go 方法 receiver 参数类型是 T 类型实例,还是 *T 类型实例,都既可以调用 receiver 为 T 类型的方法,也可以调用 receiver 为 *T 类型的方法。这样,在为方法选择 receiver 参数的类型的时候,就不需要担心这个方法不能被与 receiver 参数类型不一致的类型实例调用了。
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
类型为 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()。
# 3.2 方法集合
==方法集合是用来判断一个类型是否实现了某接口类型的唯一手段,“方法集合决定了接口实现”。==
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
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
以 int、*int 为代表的 Go 原生类型由于没有定义方法,所以它们的方法集合都是空的;
自定义类型 T 定义了方法 M1 和 M2,因此它的方法集合包含了 M1 和 M2;
自定义类型 *T 的方法集合中除了预期的 M3 和 M4 之外,还包含了类型 T 的方法 M1 和 M2。
Go 语言规定,*T 类型的方法集合包含所有以 *T 为 receiver 参数类型的方法,以及所有以 T 为 receiver 参数类型的方法。
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)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
定义了一个接口类型 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 变量。
使用 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
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
T 类型的方法集合中只包含 M1,没有 Interface 类型方法集合中的 M2 方法,这就是 Go 编译器认为变量 t 不能赋值给 Interface 类型变量的原因。
==方法集合决定接口实现==的含义就是:如果某类型 T 的方法集合与某接口类型的方法集合相同,或者类型 T 的方法集合是接口类型 I 方法集合的超集,那么就说这个类型 T 实现了接口 I。或者说,方法集合这个概念在 Go 语言中的主要用途,就是用来判断某个类型是否实现了某个接口。
选择 receiver 参数类型的原则:
选择依据就是 T 类型是否需要实现某个接口,是否存在将 T 类型的变量赋值给某接口类型变量的情况。
- 如果 T 类型需要实现某个接口,那就要使用 T 作为 receiver 参数的类型,来满足接口类型方法集合中的所有方法。
- 如果 T 不需要实现某一接口,但 *T 需要实现该接口,那么根据方法集合概念,*T 的方法集合是包含 T 的方法集合的。