Go 方法本质

方法声明

1
2
3
4
func (receiver *T或T) MethodName(参数列表) (返回值列表) {
// 方法体
}

receiver 是方法与类型之间的纽带,也是方法与函数的最大不同。无论 receiver 参数的类型为 *T 还是 T,都把一般声明形式中的 T 叫做 receiver 参数 。如果 t 的类型为 T,那么说这个方法是类型 T 的一个方法,如果 t 的类型为 *T,那么就说这个方法是类型 *T 的一个方法。每个方法只能有一个 receiver 参数。

receiver 部分的参数名不能与方法参数列表中的形参名,以及具名返回值中的变量名存在冲突,必须在这个方法的作用域中具有唯一性。

1
2
3
4
type T struct{}

func (t T) M(t string) { // 编译器报错:'t' is redeclared in this function (重复声明参数t)
}

receiver 参数的基类型不能是指针类型或接口类型。

1
2
3
4
5
6
7
8
9
type MyInt *int
func (r MyInt) String() string { // r的基类型为MyInt,编译器报错:invalid receiver type MyInt (MyInt is a pointer type)
return fmt.Sprintf("%d", *(*int)(r))
}

type MyReader io.Reader
func (r MyReader) Read(p []byte) (int, error) { // r的基类型为MyReader,编译器报错:invalid receiver type MyReader (MyReader is an interface type)
return r.Read(p)
}

方法声明要与 receiver 参数的基类型声明放在同一个包内。 所以不能给原生类型和跨越go包给别的类型申明方法。

receiver 参数的基类型为 T,那么 receiver 参数绑定在 T 上,可以通过 *T 或 T 的变量实例调用该方法:

1
2
3
4
5
6
7
8
9
10
11
12
type T struct{}

func (t T) M(n int) {
}

func main() {
var t T
t.M(1) // 通过类型T的变量实例调用方法M

p := &T{}
p.M(2) // 通过类型*T的变量实例调用方法M
}

方法本质

Go语言中的方法本质是,一个以方法的 receiver 参数作为第一个参数的普通函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 类型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
}

var t T
t.Get()
(&t).Set(1)

// 等价替换

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

一个以方法的 receiver 参数作为第一个参数的普通函数。调用方式这种直接以类型名 T 调用方法的表达方式,被称为 Method Expression。

Method Expression 有些类似于 C++ 中的静态方法(Static Method),C++ 中的静态方法在使用时,以该 C++ 类的某个对象实例作为第一个参数,而 Go 语言的 Method Expression 在使用时,同样以 receiver 参数所代表的类型实例作为第一个参数。

方法自身的类型就是一个普通函数的类型,甚至可以将它作为右值,赋值给一个函数类型的变量。

1
2
3
4
5
6
7
8
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
f1(&t, 3) // t.a=3
fmt.Println(f2(t)) // 3
}

如何选择receiver类型

1
2
func (t T) M1() <=> F1(t T)
func (t *T) M2() <=> F2(t *T)

以 T 作为 receiver 参数类型时,M1 方法等价转换为 F1(t T),Go 函数的参数采用的是值拷贝传递,也就是说,F1 函数体中的 t 是 T 类型实例的一个副本。这样,我们在 F1 函数的实现中对参数 t 做任何修改,都只会影响副本,而不会影响到原 T 类型实例。

以 *T 作为 receiver 参数类型时,M2 方法等价转换为 F2(t *T)。传递给 F2 函数的 t 是 T 类型实例的地址, F2 函数体中对参数 t 做的任何修改,都会反映到原 T 类型实例上。

如果 Go 方法要把对 receiver 参数代表的类型实例的修改,反映到原类型实例上,那么应该选择 *T 作为 receiver 参数的类型。

无论是 T 类型实例,还是 *T 类型实例,都既可以调用 receiver 为 T 类型的方法,也可以调用 receiver 为 *T 类型的方法,当 Go 编译器发现类型不一致后会自动转换。使用中可以根据是否允许该方法修改结构体实例的值来决定是用 *T 还是 T 作为 receiver,而不必担心调用方法时该怎么使用。

1
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
 type T struct {
a int
}

func (t T) M1() {
t.a = 10
}

func (t *T) M2() {
t.a = 11
}

func main() {
var t1 T
println(t1.a) // 0
t1.M1()
println(t1.a) // 0
t1.M2() // 编译器自动转换。(&t1).M2()
println(t1.a) // 11

var t2 = &T{}
println(t2.a) // 0
t2.M1()
println(t2.a) // 0
t2.M2() // 编译器自动转换 (*t2).M1()
println(t2.a) // 11
}

如果 receiver 参数类型的 size 较大,以值拷贝形式传入就会导致较大的性能开销,选择 *T 作为 receiver 类型可能更好些。

方法集合

Go 中任何一个类型都有属于自己的方法集合。方法集合是 Go 类型的一个“属性”。但不是所有类型都有自己的方法,比如 int 类型就没有。所以,对于没有定义方法的 Go 类型,称其拥有空方法集合。方法集合是用来判断一个类型是否实现了某接口类型的唯一手段。

Go 语言规定,*T 类型的方法集合包含所有以 *T 为 receiver 参数类型的方法,以及所有以 T 为 receiver 参数类型的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Interface interface {
M1()
M2()
}

type T struct{}

func (t T) M1() {}
func (t *T) M2() {}

func main() {
var t T
var pt *T
var i Interface

i = pt
i = t // T 没有实现 Interface 类型方法列表中的 M2,因此类型 T 的实例 t 不能赋值给 Interface 变量
}

获取类型的方法集合

1
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

func GetMethod(i interface{}) {
dynTyp := reflect.TypeOf(i)

if dynTyp == nil {
fmt.Printf("there is no dynamic type\n")
return
}

n := dynTyp.NumMethod()
if n == 0 {
fmt.Printf("%s's method set is empty!\n", dynTyp)
return
}

fmt.Printf("%s's method set:\n", dynTyp)
for j := 0; j < n; j++ {
fmt.Println("-", dynTyp.Method(j).Name)
}
fmt.Printf("\n")
}

func main() {
var t T
var pt *T
GetMethod(t)
GetMethod(pt)
}

输出结果:

1
2
3
4
5
6
main.T's method set:
- M1

*main.T's method set:
- M1
- M2

Go 方法本质
http://example.com/posts/46548.html
作者
她微笑的脸y
发布于
2022年6月16日
许可协议