方法的本质
# 2.方法的本质
Go 语言中的方法的本质就是,==一个以方法的 receiver 参数作为第一个参数的普通函数。==
Go 的方法与 Go 中的类型是通过 receiver 联系在一起,可以为任何非内置原生类型定义方法,比如下面的类型 T:
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(a int) int {
t.a = a
return t.a
}
// 类型T的方法Get的等价函数
func Get(t T) int {
return t.a
}
// 类型*T的方法Set的等价函数
func Set(t *T, a int) int {
t.a = a
return t.a
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这种等价转换后的函数的类型就是方法的类型。只不过在 Go 语言中,这种等价转换是由 Go 编译器在编译和生成代码时自动完成的。
方法表达式(Method Expression)
以上面类型 T 以及它的方法为例,结合前面说过的 Go 方法的调用方式,可以得到下面代码:
var t T
t.Get()
(&t).Set(1)
2
3
用另一种方式,把上面的方法调用做一个等价替换:
var t T
T.Get(t)
(*T).Set(&t, 1)
2
3
直接以类型名 T 调用方法的表达方式,被称为 Method Expression。通过 Method Expression 这种形式,类型 T 只能调用 T 的方法集合(Method Set)中的方法,同理类型 *T 也只能调用 *T 的方法集合中的方法。
Method Expression 就是 Go 方法本质的最好体现,因为方法自身的类型就是一个普通函数的类型,甚至可以将它作为右值,赋值给一个函数类型的变量,比如下面示例:
func main() {
var t T
f1 := (*T).Set // f1的类型,也是*T类型Set方法的类型:func (t *T, int)int
f2 := T.Get // f2的类型,也是T类型Get方法的类型:func(t T)int
fmt.Printf("the type of f1 is %T\n", f1) // the type of f1 is func(*main.T, int) int
fmt.Printf("the type of f2 is %T\n", f2) // the type of f2 is func(main.T) int
f1(&t, 3)
fmt.Println(f2(t)) // 3
}
2
3
4
5
6
7
8
9
基于对方法本质的深入理解,来分析解决实际编码工作中遇到的真实问题:
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print()
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print()
}
time.Sleep(3 * time.Second)
}
/*
one
two
three
six
six
six
*/
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
问题:为什么对 data2 迭代输出的结果是三个“six”,而不是 four、five、six?
分析:
根据 Go 方法的本质,也就是一个以方法的 receiver 参数作为第一个参数的普通函数,对这个程序做个等价变换。这里利用 Method Expression 方式,等价变换后的源码如下:
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go (*field).print(v)
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go (*field).print(&v)
}
time.Sleep(3 * time.Second)
}
// 把对 field 的方法 print 的调用,替换为 Method Expression 形式,替换前后的程序输出结果是一致的。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
可以很清楚地看到使用 go 关键字启动一个新 Goroutine 时,method expression 形式的 print 函数是如何绑定参数的:
- 迭代 data1 时,由于 data1 中的元素类型是 field 指针 (*field),因此赋值后 v 就是元素地址,与 print 的 receiver 参数类型相同,每次调用 (*field).print 函数时直接传入的 v 即可,实际上传入的也是各个 field 元素的地址;
- 迭代 data2 时,由于 data2 中的元素类型是 field(非指针),与 print 的 receiver 参数类型不同,因此需要将其取地址后再传入 (*field).print 函数。这样每次传入的 &v 实际上是变量 v 的地址,而不是切片 data2 中各元素的地址。
一旦启动的各个子 goroutine 在 main goroutine 执行到 Sleep 时才被调度执行。
- 前三个子 goroutine 各自传入的是元素“one”、“two”和“three”的地址,所以打印的就是“one”、“two”和“three”;
- 最后三个 goroutine 在打印 &v 时,实际打印的也就是在 v 中存放的值“six”。
只需要*将 field 类型 print 方法的 receiver 类型由 field 改为 field ,就可以按期望输出“one”、“two”、“three”、“four”、 “five”、“six”。
type field struct {
name string
}
func (p field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print()
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print()
}
time.Sleep(3 * time.Second)
}
/*
one
two
three
four
five
six
*/
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