方法的 receiver
Go 语言中的方法是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者。接收者的概念就类似于其他语言中的 this
或 self
。
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"}
。