Go基础|第10章:method
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) *T
,T 不能是像 *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 类型值也可以正常调用?如果是函数调用,实参类型必须跟形参类型一致才行
为了符合方法接收者的定义,Go 编译器会将 v.Scale(10)
解释为 (&v).Scale(10)
。但是,这种简写方法只适用于接收者 可寻址(addressable)
时(比如变量 v、struct 的字段 s.X、array/slice 的元素 a[0]),我们不能通过一个 不可寻址(not addressable)
的接收者来调用指针方法,比如无法获取 map 的元素、临时变量的内存地址:
同样的道理,可以使用指针类型 *Vertex 调用值接收者的方法 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
orstring
, 它们本质上是一种很原始的数据值,所以在函数或方法内外传递时,要拷贝一份副本 - for
map
,function
andchannel
types, 当声明引用类型的变量时,创建的变量被称作标头(header)
值,每个引用类型创建的标头值都包含一个指向底层数据结构的指针。每个引用类型还包含一组独特的字段,用于管理底层数据结构。因为标头值就是为复制而设计的,所以永远不需要共享一个引用类型的值。标头值里包含一个指针,因此通过复制来传递一个引用类型的值的副本,本质上就是在共享底层数据结构 - for small
arrays
orstructs
that are value types, with no mutable fields and no pointers.
- for simple basic types such as
- You may want to use value receivers
- for
slice
with methods that do not reslice or reallocate the slice. 比如,方法内使用了 append 函数
- for
4. 方法集
方法签名(method signature):
由方法名、形式参数列表、返回值列表组成,形参和返回值的 变量名
不影响方法签名,比如:
方法集(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() 方法
方法集代表了类型是否隐式地实现 接口,详情见下一篇博文
0 条评论
评论者的用户名
评论时间暂时还没有评论.