「读书笔记」理解方法的本质以选择正确的 receiver 类型

313 阅读4分钟

函数与方法

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 的调用替换为方法表达式的形式,立刻豁然开朗了。

往期回顾

关注我

掘金:XQGang

Github: XQ-Gang

参考

《Go 语言精进之路:从新手到高手的编程思想、方法和技巧》——白明