从 90 年代早期开始,面向对象编程(OOP)就成为了称霸工程界和教育界的编程范式,所以之后几乎所有大规模被应用的语言都包含了对 OOP 的支持,go 语言也不例外。OOP 编程的第一方面,我们会向你展示如何有效地定义和使用方法。我们会覆盖到 OOP 编程的两个关键点,封装和组合。
方法声明
- 在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。
package geometry import "math" type Point struct{ X, Y float64 } // traditional functionfunc 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 typefunc (p Point) Distance(q Point) float64 { return math.Hypot(q.X-p.X, q.Y-p.Y) } - 上面的代码里那个附加的参数 p,叫做方法的接收器(receiver),早期的面向对象语言留下的遗产将调用一个方法称为“向一个对象发送消息”。
- 在 go 语言中不适用 this self 做方法接收器,它建议适用类型的首字母作为接收器简短一致。
- 包级的方法不会跟类型的方法产生混乱
// 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 } perim := Path{ {1, 1}, {5, 1}, {5, 4}, {1, 1}, } fmt.Println(perim.Distance()) // "12" - 在上面两个对 Distance 名字的方法的调用中,编译器会根据方法的名字以及接收器来决定具体调用的是哪一个函数。第一个例子中 path[i-1]数组中的类型是 Point,因此 Point.Distance 这个方法被调用;在第二个例子中 perim 的类型是 Path,因此 Distance 调用的是 Path.Distance。
- 对于一个给定的类型,其内部的方法都必须有唯一的方法名,但是不同的类型却可以有同样的方法名,比如我们这里 Point 和 Path 就都有 Distance 这个名字的方法;所以我们没有必要非在方法名之前加类型名来消除歧义,比如 PathDistance。
import "gopl.io/ch6/geometry" perim := geometry.Path{{1, 1}, {5, 1}, {5, 4}, {1, 1}} fmt.Println(geometry.PathDistance(perim)) // "12", standalone function fmt.Println(perim.Distance()) // "12", method of geometry.Path - 如果我们要用方法去计算 perim 的 distance,还需要去写全 geometry 的包名,和其函数名,但是因为 Path 这个变量定义了一个可以直接用的 Distance 方法,所以我们可以直接写 perim.Distance()。相当于可以少打很多字,作者应该是这个意思。因为在 Go 里包外调用函数需要带上包名,还是挺麻烦的。
基于指针的对象方法
- 不管你的 method 的 receiver 是指针类型还是非指针类型,都是可以通过指针 /非指针类型进行调用的,编译器会帮你做类型转换。在声明一个 method 的 receiver 该是指针还是非指针类型时,你需要考虑两方面的内部,
- 第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;
- 第二方面是如果你用指针类型作为 receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。熟悉 C 或者 C 艹的人这里应该很快能明白。
