Go基础|第10章:method

  • 原创
  • Madman
  • /
  • /
  • 0
  • 4969 次阅读

Golang.jpg

Synopsis: 方法能给用户定义的类型添加新的行为,它有两种类型的接收者:值接收者(value receiver) 和指针接收者(pointer receiver),本文将介绍在 Go 中和方法相关的各种概念

1. 方法声明

Go 语言没有类的概念,但是你可以为 同一个包内 的任意 命名类型(defined type) 定义 方法(method),只要这个 命名类型的底层类型不是 pointer 或 interface,方法能给 用户定义的类型 添加新的行为

方法实际上也是函数,只是在声明时,在关键字 func 和方法名之间增加了一个 接收者(receiver) 参数。接收者参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独有的方法,使用点号调用方法:

package main

import (
  "fmt"
  "math"
)

type Vertex struct {
  X, Y float64
}

// Abs 方法拥有一个名为 v,类型为 Vertex 的接收者
// 由于接收者的名字经常会被使用到,所以保持其在方法间传递时的一致性和简短性是不错的主意。建议使用其类型的第一个字母,比如这里使用了 Vertex 的首字母 v,不要像其它语言那样用 this 或者 self 作为接收者
func (v Vertex) Abs() float64 {
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// 方法只是个带接收者参数的函数。上面的方法改写为函数写法,功能一样
func Abs(v Vertex) float64 {
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
  v := Vertex{3, 4}
  fmt.Println(v.Abs()) // 调用 Vertex 类下面声明的 Vertex.Abs() 方法
  fmt.Println(Abs(v))  // 调用包级别的函数 main.Abs(),所以与 Vertex.Abs() 并不冲突
}

/* Output:
5
5
*/

由于方法和字段都是在同一命名空间,所以如果我们把方法名 Abs 改为 X 的话,编译器会报错: type Vertex has both field and method named X

也可以为非结构体类型声明方法,但是只能为在同一包内定义的类型声明方法,而不能为其它包内定义的类型(包括 int 等基本类型)声明方法,即接收者的类型定义和方法声明必须在同一包内;不能为内建的基本类型声明方法

package main

import (
  "fmt"
  "math"
)

// 我们不能给 float64 这种内建基本类型声明方法,但是可以给命名类型 MyFloat 声明方法
type MyFloat float64

func (f MyFloat) Abs() float64 { // 如果把接收者类型 MyFloat 改为 float64 时: cannot define new methods on non-local type float64
  if f < 0 {
    return float64(-f)
  }
  return float64(f)
}

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

2. 值接收者和指针接收者

Go 语言里有两种类型的接收者:值接收者(value receiver) T 和指针接收者(pointer receiver) *TT 不能是像 *int 这样的指针,否则会编译报错:invalid receiver type T (T is a pointer type)

如果使用 值接收者 声明方法,调用方法时只会对这个值的 副本 进行操作,不会影响原始值;而 指针接收者 的方法可以修改接收者指向的值。由于方法经常需要修改它的接收者,指针接收者比值接收者更常用

package main

import (
  "fmt"
  "math"
)

type Vertex struct {
  X, Y float64
}

// 值接收者。方法名为 Vertex.Abs
func (v Vertex) Abs() float64 {
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// 指针接收者,方法会修改指针所指向的值。方法名为 (*Vertex).Scale
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)          // 此时 v = Vertex{30, 40}。如果 Scale 方法的接收者改为 v Vertex,那么这里的 v 还是 Vertex{3, 4}
  fmt.Println(v.Abs()) // 50
}

你会发现 Scale() 方法声明时使用了指针接收者,为什么我们直接用 Vertex 类型值也可以正常调用?如果是函数调用,实参类型必须跟形参类型一致才行

v := Vertex{3, 4}
v.Scale(10)

为了符合方法接收者的定义,Go 编译器会将 v.Scale(10) 解释为 (&v).Scale(10)。但是,这种简写方法只适用于接收者 可寻址(addressable) 时(比如变量 v、struct 的字段 s.X、array/slice 的元素 a[0]),我们不能通过一个 不可寻址(not addressable) 的接收者来调用指针方法,比如无法获取 map 的元素、临时变量的内存地址:

Vertex{3, 4}.Scale(10) // cannot call pointer method on Vertex literal

Which values can and which values can't be taken addresses?

同样的道理,可以使用指针类型 *Vertex 调用值接收者的方法 Abs():

p := &v
p.Abs()

这种情况下,方法调用 p.Abs() 会被解释为 (*p).Abs(),永远可以通过地址解引用来找到对应的值

综上所述,不管是 还是 指针,都能够调用 使用值接收者声明的方法使用指针接收者声明的方法,Go 编译器会帮你做类型转换

3. 如何选择方法的接收者

https://github.com/golang/go/wiki/CodeReviewComments#receiver-type

Go 语言既允许使用值,也允许使用指针来 调用 方法,不必严格符合接收者的类型,这个支持非常方便开发者编写程序。但是,我们给类型定义方法时,应该使用值接收者,还是应该使用指针接收者呢?

3.1 基本准则

  • For a given type, don’t mix value and pointer receivers. 上面示例中 Vertex 只是为了演示方法可以有两种接收者而已,实际代码中不要混用两种接收者
  • If in doubt, use pointer receivers (they are safe and extendable).

3.2 使用指针接收者

  • You must use pointer receivers
    • if the method needs to mutate the receiver,
    • for structs that contain a sync.Mutex or similar synchronizing field, the receiver must be a pointer to avoid copying. (sync.Mutex musn’t be copied)
  • You probably want to use pointer receivers
    • for large structs or arrays (it can be more efficient),
    • in all other cases.

3.3 使用值接收者

  • You probably want to use value receivers
    • for simple basic types such as int or string, 它们本质上是一种很原始的数据值,所以在函数或方法内外传递时,要拷贝一份副本
    • for map, function and channel types, 当声明引用类型的变量时,创建的变量被称作 标头(header) 值,每个引用类型创建的标头值都包含一个指向底层数据结构的指针。每个引用类型还包含一组独特的字段,用于管理底层数据结构。因为标头值就是为复制而设计的,所以永远不需要共享一个引用类型的值。标头值里包含一个指针,因此通过复制来传递一个引用类型的值的副本,本质上就是在共享底层数据结构
    • for small arrays or structs that are value types, with no mutable fields and no pointers.
  • You may want to use value receivers
    • for slice with methods that do not reslice or reallocate the slice. 比如,方法内使用了 append 函数

4. 方法集

方法签名(method signature):

由方法名、形式参数列表、返回值列表组成,形参和返回值的 变量名 不影响方法签名,比如:

Abs() float64
Scale(float64)

方法集(method set) 定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联

Values Methods Receivers
T (t T)
*T (t T) and (t *T)

Go 语言规范里定义的方法集的规则:T 类型的值的方法集只包含使用值接收者声明的方法(并不是每个值都是可寻址的),而指向 T 类型的指针 *T 的方法集既包含使用值接收者声明的方法(永远可以通过地址解引用来找到对应的值),也包含使用指针接收者声明的方法。类型 T 的方法集总是类型 *T 的方法集的子集

上例中值类型 Vertex 的方法集只包含 Abs() 方法,而指针类型 *Vertex 的方法集包含 Abs() 和 Scale() 方法

方法集代表了类型是否隐式地实现 接口,详情见下一篇博文

未经允许不得转载: LIFE & SHARE - 王颜公子 » Go基础|第10章:method

分享

作者

作者头像

Madman

如需 Linux / Python 相关问题付费解答,请按如下方式联系我

0 条评论

暂时还没有评论.