函数与方法
23 理解方法的本质以选择正确的 receiver 类型
Go 语言中的方法在声明形式上比函数仅多了一个参数,这个参数被称为 receiver 参数。receiver 参数是方法和类型之间的纽带。
方法的特点
Go 方法具有如下特点:
- 方法名的首字母是否大写决定了该方法是不是导出方法。
- 方法定义要与类型定义放在同一个包内,因此,不能为原生类型(如 int、float64、map 等)添加方法,只能为自定义类型定义方法。同理,不能横跨 Go 包为其他包内的自定义类型定义方法。
- 每个方法只能有一个 receiver 参数,不支持多 receiver 参数列表或变长 receiver 参数。一个方法只能绑定一个基类型,Go 语言不支持同时绑定多个类型的方法。
- receiver 参数的基类型本身不能是指针类型或接口类型。
方法的本质
一个以方法所绑定类型实例为第一个参数的普通函数。
我们将 receiver 作为第一个参数传入方法的参数列表,如下所示:
type T struct {
a int
}
func (t *T) Set(a int) int {
t.a = a
return t.a
}
// 等价转换
func Set(t *T, a int) int {
t.a = a
return t.a
}
这种转换后的函数就是方法的原型。这种等价转换是由 Go 编译器在编译和生成代码时自动完成的。Go 语言规范中提供了一个新概念,可以让我们更充分地理解上面的等价转换。
var t T
t.Get()
t.Set(1)
// 等价转换
var t T
T.Get(t)
(*T).Set(&t, 1)
这种直接以类型名 T 调用方法的表达方式被称为方法表达式。类型 T 只能调用 T 的方法集合中的方法,同理,*T 只能调用 *T 的方法集合中的方法。
选择正确的 receiver 类型
方法和函数的等价变换公式:
func (t T) M1() <=> M1(t T)
func (t *T) M2() <=> M2(t *T)
M1 方法的 receiver 参数类型为 T,而 M2 方法的参数类型为 *T。
-
当 receiver 参数的类型为 T 时,选择值类型的 receiver。Go 函数的参数采用的是值复制传递,也就是说 M1 函数体中的 t 是 T 类型实例的一个副本,这样在 M1 函数的实现中对参数 t 做任何修改都只会影响副本,而不会影响到原 T 类型实例。
-
当 receiver 参数的类型为 *T 时,选择指针类型的 receiver。我们传递给 M2 函数的 t 是 T 类型实例的地址,这样 M2 函数体中对参数 t 做的任何修改都会反映到原 T 类型实例上。
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
t.M2()
println(t.a) // 11
}
无论是 T 类型实例还是 *T 类型实例,都既可以调用 receiver 为 T 类型的方法,也可以调用 receiver 为 *T 类型的方法。实际上这都是 Go 语法糖,Go 编译器在编译和生成代码时为我们自动做了转换。
综上,receiver 类型选用的初步结论:
- 如果要对类型实例进行修改,那么为 receiver 选择 *T 类型;
- 如果没有对类型实例修改的需求,那么为 receiver 选择 T 类型或 *T 类型均可;但考虑到 Go 方法调用时,receiver 是以值复制的形式传入方法中的,如果类型的 size 较大,以值形式传入会导致较大损耗,这时选择 *T 作为 receiver 类型会更好些。
关于 receiver 类型选择还有一个重要因素,那就是类型是否要实现某个接口。
基于对 Go 方法本质的理解巧解难题
type field struct {
name string
}
func (p field) print1() {
fmt.Println(p.name)
}
func (p *field) print2() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print1() // one two three
// 等价转换
go field.print1(*v) // one two three
}
for _, v := range data1 {
go v.print2() // one two three
// 等价转换
go (*field).print1(v) // one two three
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print1() // four five six
// 等价转换
go field.print1(v) // four five six
}
for _, v := range data2 {
go v.print2() // six six six
// 等价转换
go (*field).print1(&v) // six six six
}
time.Sleep(1 * time.Second)
}
我们把对类型 field 的方法 print 的调用替换为方法表达式的形式,立刻豁然开朗了。
往期回顾
关注我
参考
《Go 语言精进之路:从新手到高手的编程思想、方法和技巧》——白明