方法

Go 语言没有类的概念。 但是,你可以为某个类型定义 方法 ( method )。

方法 是一个带 接收者参数 的特殊函数。 接收者参数位于 func 关键字与方法名之间,以括号包围。

下面这个例子中, Abs 方法有一个 Vertex 类型的接收者参数 v

/_src/tour/methods.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 ,将其实现成一个普通函数,功能上并没有任何区别:

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

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

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

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

非结构体方法

不仅 结构体 可以定义方法,其他任何自定义类型均可。

以下就是一例,为数值类型 MyFloat 定义方法 Abs

/_src/tour/methods-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())
}

方法和对应类型定义必须在同一个 定义。

指针接收者

方法接收者可以定义成 指针

这样一来,对于类型 T 来说,接收者参数的类型就是 *T 。 需要注意的是, T 本身不能是指针,比如 *int

例子中, Scale 方法就定义在 *Vertex 上:

/_src/tour/methods-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())
 }

接收者参数定义成指针的好处是,方法代码可以修改指针指向的值。 由于方法经常需要修改对应的值,因此指针接收者参数相对来说更常用。

读者可以自行修改程序,将 * 号从 Scale 方法移除,并观察程序行为。 不出意外,你将看到程序输出 5 。换句话讲,并没有修改到目标值。 这是为啥呢?

如果定义值接收者( value receiver ), Scale 方法相当于在原 Vertex 值的一个拷贝上操作(适用于其他参数)。 因此,为了修改 Vertex 值,接收者参数必须定义成指针。

传值与传引用

接下来,我们将 AbsScale 方法重写成普通函数。

/_src/tour/methods-pointers-explained.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 Abs(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

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

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

同样,将 * 号从 Scale 函数移除会怎样? 不出意外,结果是类似的。

这其实是编程里最经典的 传值传引用 问题, 传指针相当于传引用

间接传指针

对比上面两个程序,你可能已经注意到了——带指针参数的函数只能传指针:

var v Vertex
ScaleFunc(v, 5)     // Compile error!
ScaleFunc(&v, 5)    // OK

然而,对于方法,不管接收者是一个值还是指针,均可调用:

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) 。 这就是 间接传指针 ,或者叫做 隐式传指针

/_src/tour/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)
}

间接传值

对普通 函数 来说,值参数只能传对应类型的值,传指针则导致编译错误:

var v Vertex
fmt.Println(AbsFunc(v))     // OK
fmt.Println(AbsFunc(&v))    // Compile error!

相反,就算方法定义了值接收者参数,用指针调用也是可以的:

var v Vertex
fmt.Println(v.Abs())    // OK

p := &v
fmt.Println(p.Abs())    // OK

在这,方法调用语句 p.Abs() 则被解释成: (*p).Abs() 。 这就是 间接传值 ,或者叫做 隐式传值

/_src/tour/indirection-values.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
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 AbsFunc(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

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

    p := &Vertex{4, 3}
    fmt.Println(p.Abs())
    fmt.Println(AbsFunc(p))
}

传值还是传指针

那么,接收者参数到底是实现成值还是指针呢? 如何选择?

使用指针接收者参数主要有两方面考虑:

首先,只有这种方式能够对指向的值进行修改。

其次,从性能方面考虑,使用指针可以避免在每次调用方法时拷贝值。 这种方式相对来说更高效,特别是当接收者 结构体 很大很复杂时。

在这个例子, Scale 方法和 Abs 方法接收者参数类型均为 *Vertex ,尽管 Abs 方法并不修改其接收者:

/_src/tour/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.Println("Before scaling: %+v, Abs: %v\n", v, v.Abs())
    v.Scale(5)
    fmt.Println("After scaling: %+v, Abs: %v\n", v, v.Abs())
}

通常,不管为何种类型编写方法,均需要定义 值接收者 或者 指针接收者 ,不能混用。

下一步

下一节 我们一起来看看 Go 语言 interfaces

订阅更新,获取更多学习资料,请关注我们的 微信公众号

../_images/wechat-mp-qrcode.png

小菜学编程

微信打赏