这是我参与「第三届青训营 -后端场」笔记创作活动的第4篇笔记。
方法
方法声明
在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。可以类比 Java 中对象的方法。
package geometry
import "math"
type Point struct {
X, Y float64
}
// traditional function
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
上面的代码里那个附加的参数p,叫做方法的接收器(receiver),早期的面向对象语言留下的遗产将调用一个方法称为“向一个对象发送消息”。
在Go语言中,不会像其它语言那样用 this 或者 self 作为接收器;可以任意选择接收器的名字。由于接收器的名字经常会被使用到,建议使用其类型的第一个字母,比如这里使用了 Point 的首字母 p 。
在方法调用过程中,接收器参数一般会在方法名之前出现。下面是例子:
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", function call 函数调用
fmt.Println(p.Distance(q)) // "5", method call 方法调用,p 为接收器参数
第一个 Distance 的调用实际上用的是包级别的函数 geometry.Distance,而第二个则是使用刚刚声明的 Point,调用的是 Point 类下声明的 Point.Distance 方法。
这种 p.Distance 的表达式叫做选择器,因为他会选择合适的对应 p 这个对象的 Distance 方法来执行。选择器也会被用来选择一个 struct 类型的字段,比如p.X。由于方法和字段都是在同一命名空间,所以如果我们在这里声明一个 X 方法的话,编译器会报错,因为在调用 p.X 时会有歧义。
每种类型都有其方法的命名空间,在用 Distance 这个名字的时候,不同的 Distance 调用指向了不同类型里的 Distance 方法。下面再定义一个Path类型,这个Path代表一个线段的集合,并且也给这个 Path 定义一个叫 Distance 的方法。
// A Path is a journey connecting the points with straight lines.
type Path []Point // Distance returns the distance traveled along the path.
func (path Path) Distance() float64 {
sum := 0.0
for i := range path {
if i > 0 {
sum += path[i-1].Distance(path[i])
}
}
return sum
}
Path 是 slice 类型,而不是 Point 那样的 struct 类型,然而依然可以为它定义方法。在能够给任意类型定义方法这一点上,Go 和很多其它的面向对象的语言不太一样。因此在Go语言里,方法可以被声明到任意类型,只要不是一个指针或者一个interface。
两个 Distance 方法有不同的类型。他们两个方法之间没有任何关系,尽管 Path 的 Distance 方法会在内部调用Point.Distance 方法来计算每个连接邻接点的线段的长度。
基于指针对象的方法
当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝,这种情况下我们就需要用到指针了。对应到这里用来更新接收器的对象的方法,当这个接受者变量本身比较大时,我们就可以用其指针而不是对象来声明方法,如下:
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
如果接收器 p 是一个 Point 类型的变量,并且其方法需要一个 Point 指针作为接收器,可以用下面这种简短的写法:
p.ScaleBy(2)
编译器会隐式地帮我们用 &p 去调用 ScaleBy 这个方法。这种简写方法只适用于“变量”。不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到:
Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal
总结一下:
- 不管你的 method 的 receiver 是指针类型还是非指针类型,都可以通过指针/非指针类型进行调用的,编译器会帮你做转换。
- 在声明一个 method 的 receiver 应该是指针还是非指针类型时,你需要考虑两方面的因素,
- 这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;
- 如果指针类型作为 receiver ,那么一定要注意,这种指针类型指向的始终是一块内存地址。