方法

Go 没有类。不过你可以为类型定义方法。

方法就是一类带特殊的 接收者 参数的函数。

方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。

在此例中,Abs 方法拥有一个名字为 v,类型为 Vertex 的接收者。

methods.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}

方法即函数

方法只是个带接收者参数的函数。 现在这个Abs的写法就是一个正常的函数,功能并没有什么变化。

为非结构体类型声明方法

method-continued.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}

在此例中,我们看到了一个带 Abs 方法的数值类型 MyFloat。

你只能为在同一个包中定义的接收者类型声明方法,而不能为其它别的包中定义的类型 (包括 int 之类的内置类型)声明方法。

接收者的类型定义和方法声明必须在同一包内。

指针类型的接收者

你可以为指针类型的接收者声明方法。

这意味着对于某类型 T,接收者的类型可以用 *T 的文法。 (此外,T 本身不能是指针,比如不能是 *int。)

例如,这里为 *Vertex 定义了 Scale 方法。

指针接收者的方法可以修改接收者指向的值(如这里的 Scale 所示)。 由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。

试着移除第 16 行 Scale 函数声明中的 *,观察此程序的行为如何变化。

若使用值接收者,那么 Scale 方法会对原始 Vertex 值的副本进行操作。(对于函数的其它参数也是如此。)Scale 方法必须用指针接收者来更改 main 函数中声明的 Vertex 的值。

method-pointers.go
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
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}

方法与指针重定向

带指针参数的函数必须接受一个指针

1
2
3
var v Vertex
ScaleFunc(v, 5) // 编译错误!
ScaleFunc(&v, 5) // OK

而接收者为指针的的方法被调用时,接收者既能是值又能是指针:

1
2
3
4
var v Vertex
v.Scale(5) // OK
p := &v
p.Scale(10) // OK

对于语句 v.Scale(5) 来说,即便 v 是一个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale 方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)

indirection.go
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
package main

import "fmt"

type Vertex struct {
X, Y float64
}

func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func main() {
v := Vertex{3, 4}
v.Scale(2)
ScaleFunc(&v, 10)

p := &Vertex{4, 3}
p.Scale(3)
ScaleFunc(p, 8)

fmt.Println(v, p)
}

反之也一样:

接受一个值作为参数的函数必须接受一个指定类型的值:

1
2
3
var v Vertex
fmt.Println(AbsFunc(v)) // OK
fmt.Println(AbsFunc(&v)) // 编译错误!

而以值为接收者的方法被调用时,接收者既能为值又能为指针:

1
2
3
4
var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK

这种情况下,方法调用 p.Abs() 会被解释为 (*p).Abs()

选择值或指针作为接收者

使用指针接收者的原因有二:

首先,方法能够修改其接收者指向的值。

其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样会更加高效。

在本例中,ScaleAbs 接收者的类型为 *Vertex,即便 Abs 并不需要修改其接收者。

通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。

methods-with-pointer-receivers.go
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
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := &Vertex{3, 4}
fmt.Printf("缩放前:%+v,绝对值:%v\n", v, v.Abs())
v.Scale(5)
fmt.Printf("缩放后:%+v,绝对值:%v\n", v, v.Abs())
}