Go 语言
函数和方法
方法的 receiver

方法的 receiver

Go 语言中的方法是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者。接收者的概念就类似于其他语言中的 thisself

Go 称之为 receiver(参数)。 receiver 是方法与类型之间的纽带。

方法的定义格式如下:

func (接收者变量 T/*T) MethodName(参数列表) (返回参数) {
    // 方法体
}
 
var t T
t.MethodName(参数列表)
 
var pt *T = &t
pt.MethodName(参数列表)

Go 方法具有如下特点:

  • 方法名的首字母是否大写决定了该放是不是导出方法;
  • 方法定义要与类型定义放在同一个包内,不能为原生类型(int、float64、map)等添加方法,只能为自定义类型定义方法;不能横跨 Go 包为其他包内的类型定义方法;
  • 每个方法只能有一个 receiver,不支持多个 receiver 参数列表或变长 receiver 参数;
  • receiver 参数的基本类型本身不能是指针类型或接口类型;

方法的本质

Go 语言没有类,方法与类型通过 receiver 联系在一起。

我们来看一个例子:

type T struct {
    a int
}
 
func (t T) Get() int {
    return t.a
}
 
func (t *T) Set(i int) {
    t.a = i
}
 
func main() {
    var t T
    t.Set(1)
    fmt.Println(t.Get())
}

上面例子中的类型 T 的方法可以等价于下面的普通函数:

func Get(t T) int {
    return t.a
}
 
func Set(t *T, i int) {
    t.a = i
}

这种转换后的函数就是方法的本质。这种等价转换是由 Go 编译器在编译和生成代码时自动完成的。

我们可以用下面方式等价替换上面的方法调用:

var t T
(*T).Set(&t, 1)
fmt.Println(T.Get(t))

这种以类型名 T 调用方法的表达式称为方法表达式。类型 T 只能调用 T 的方法集合中的方法,不能调用 *T 的方法。

所以 Go 方法的本质是一个以方法所绑定类型实例为第一个参数的普通函数。

选择 receiver 类型

我们看一下方法和函数的等价交换公式:

func (t T) M1() <=> M1(t T)
func (t *T) M2() <=> M2(t *T)

M1 方法的 receiver 是 T 类型,是值类型;M2 方法的 receiver 是 *T 类型,是指针类型。

1、当 receiver 参数为 T 类型时,方法内部无法修改接收者的值,因为 receiver 是值传递的,方法内部修改的是 receiver 的副本。 2、当 receiver 参数为 *T 类型时,方法内部可以修改接收者的值,因为 receiver 是指针传递的,方法内部修改的是 receiver 的真实值。

我们来看一个例子:

type T struct {
    a int
}
 
func (t T) Get() int {
    return t.a
}
 
func (t T) Set1(i int) {
    t.a = i
}
 
func (t *T) Set2(i int) {
    t.a = i
}
 
func main() {
    var t T // a = 0
    println(t.a)
 
    t.Set1(6)
    println(t.a)
 
    t.Set2(8)
    println(t.a)
}
 
// 输出
// 0
// 0
// 8

还有一点:无论是 T 类型实例还是 *T 类型实例,都可以调用 T 类型的方法集合中的方法,也可以调用 *T 类型的方法集合中的方法。

func main() {
    var t T
    r.Set1(1)
    r.Set2(2) // ok
 
    var pt = &T{}
    pt.Set1(3)
    pt.Set2(4) // ok
}

实际上,这是 Go 语法糖,Go 编译器在编译和生成代码时为我们自动做了转换。

那么我们得出 receiver 类型选择的初步结论:

  • 如果要对类型实例进行修改,那么为 receiver 选择 *T 类型,是指针类型。
  • 如果没有对类型实例修改的需求,那么为 receiver 选择 T 类型还是 *T 类型都可以。如果考虑 receiver 是以值复制的方式传入方法中,如果类型 size 较大,那么选择 *T 类型会更高效。

难题理解

我们来看一个例子:

import "time"
 
type field struct {
	name string
}
 
func (p *field) print() {
	println(p.name)
}
 
func main() {
	m1 := []*field{{"one"}, {"two"}, {"three"}}
	for _, v := range m1 {
		go v.print()
	}
 
	m2 := []field{{"four"}, {"five"}, {"six"}}
	for _, v := range m2 {
		go v.print()
	}
 
	time.Sleep(3 * time.Second)
}
 
// 输出
// three
// six
// six
// two
// one
// six

这里出现了3个 six,为什么呢?

for _, v := range m2 {
    go v.print()
}

等价于

for _, v := range m2 {
    go (*field).print(&v)
}

&v 取的是 v 的变量地址,而 v 的变量来自 m2 的副本。 各子 goroutine 在 main goroutine 执行到 sleep 时才会被调度执行,此时 v 的值已经是 m2 的最后一个元素 {"six"}