方法

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

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

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

/_src/tour/methods.go

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "math"
  6. )
  7.  
  8. type Vertex struct {
  9. X, Y float64
  10. }
  11.  
  12. func (v Vertex) Abs() float64 {
  13. return math.Sqrt(v.X*v.X + v.Y*v.Y)
  14. }
  15.  
  16. func main() {
  17. v := Vertex{3, 4}
  18. fmt.Println(v.Abs())
  19. }

再次强调: 方法只是一个带有接收者参数的函数而已

你可以重写 Abs ,将其实现成一个普通函数,功能上并没有任何区别:

/_src/tour/methods-funcs.go

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "math"
  6. )
  7.  
  8. type Vertex struct {
  9. X, Y float64
  10. }
  11.  
  12. func Abs(v Vertex) float64 {
  13. return math.Sqrt(v.X*v.X + v.Y*v.Y)
  14. }
  15.  
  16. func main() {
  17. v := Vertex{3, 4}
  18. fmt.Println(Abs(v))
  19. }

非结构体方法

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

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

/_src/tour/methods-continued.go

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "math"
  6. )
  7.  
  8. type MyFloat float64
  9.  
  10. func (f MyFloat) Abs() float64 {
  11. if f < 0 {
  12. return float64(-f)
  13. }
  14. return float64(f)
  15. }
  16.  
  17. func main() {
  18. f := MyFloat(-math.Sqrt2)
  19. fmt.Println(f.Abs())
  20. }

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

指针接收者

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

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

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

/_src/tour/methods-pointers.go

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "math"
  6. )
  7.  
  8. type Vertex struct {
  9. X, Y float64
  10. }
  11.  
  12. func (v Vertex) Abs() float64 {
  13. return math.Sqrt(v.X*v.X + v.Y*v.Y)
  14. }
  15.  
  16. func (v *Vertex) Scale(f float64) {
  17. v.X = v.X * f
  18. v.Y = v.Y * f
  19. }
  20.  
  21. func main() {
  22. v := Vertex{3, 4}
  23. v.Scale(10)
  24. fmt.Println(v.Abs())
  25. }

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

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

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

传值与传引用

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

/_src/tour/methods-pointers-explained.go

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "math"
  6. )
  7.  
  8. type Vertex struct {
  9. X, Y float64
  10. }
  11.  
  12. func Abs(v Vertex) float64 {
  13. return math.Sqrt(v.X*v.X + v.Y*v.Y)
  14. }
  15.  
  16. func Scale(v *Vertex, f float64) {
  17. v.X = v.X * f
  18. v.Y = v.Y * f
  19. }
  20.  
  21. func main() {
  22. v := Vertex{3, 4}
  23. Scale(&v, 10)
  24. fmt.Println(Abs(v))
  25. }

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

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

间接传指针

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

  1. var v Vertex
  2. ScaleFunc(v, 5) // Compile error!
  3. ScaleFunc(&v, 5) // OK

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

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

对于语句 v.Scale(5) ,尽管 v 是一个值而不是指针,还是自动调用了带指针接收者参数的方法。这是因为,Scale 方法需要指针接收者参数, Go 按照惯例将 v.Scale(5) 解释成: (&v).Scale(5) 。这就是 间接传指针 ,或者叫做 隐式传指针

/_src/tour/indirection.go

  1. package main
  2.  
  3. import "fmt"
  4.  
  5. type Vertex struct {
  6. X, Y float64
  7. }
  8.  
  9. func (v *Vertex) Scale(f float64) {
  10. v.X = v.X * f
  11. v.Y = v.Y * f
  12. }
  13.  
  14. func ScaleFunc(v *Vertex, f float64) {
  15. v.X = v.X * f
  16. v.Y = v.Y * f
  17. }
  18.  
  19. func main() {
  20. v := Vertex{3, 4}
  21. v.Scale(2)
  22. ScaleFunc(&v, 10)
  23.  
  24. p := &Vertex{4, 3}
  25. p.Scale(3)
  26. ScaleFunc(p, 8)
  27.  
  28. fmt.Println(v, p)
  29. }

间接传值

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

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

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

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

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

/_src/tour/indirection-values.go

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "math"
  6. )
  7.  
  8. type Vertex struct {
  9. X, Y float64
  10. }
  11.  
  12. func (v Vertex) Abs() float64 {
  13. return math.Sqrt(v.X*v.X + v.Y*v.Y)
  14. }
  15.  
  16. func AbsFunc(v Vertex) float64 {
  17. return math.Sqrt(v.X*v.X + v.Y*v.Y)
  18. }
  19.  
  20. func main() {
  21. v := Vertex{3, 4}
  22. fmt.Println(v.Abs())
  23. fmt.Println(AbsFunc(v))
  24.  
  25. p := &Vertex{4, 3}
  26. fmt.Println(p.Abs())
  27. fmt.Println(AbsFunc(p))
  28. }

传值还是传指针

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

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

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

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

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

/_src/tour/methods-with-pointer-receivers.go

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "math"
  6. )
  7.  
  8. type Vertex struct {
  9. X, Y float64
  10. }
  11.  
  12. func (v *Vertex) Scale(f float64) {
  13. v.X = v.X * f
  14. v.Y = v.Y * f
  15. }
  16.  
  17. func (v *Vertex) Abs() float64 {
  18. return math.Sqrt(v.X*v.X + v.Y*v.Y)
  19. }
  20.  
  21. func main() {
  22. v := Vertex{3, 4}
  23. fmt.Println("Before scaling: %+v, Abs: %v\n", v, v.Abs())
  24. v.Scale(5)
  25. fmt.Println("After scaling: %+v, Abs: %v\n", v, v.Abs())
  26. }

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

下一步

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

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

../_images/wechat-mp-qrcode.png小菜学编程

微信打赏