目录
方法概述
尽管Go没有统一的面向对象编程(OOP)的定义,但依然支持方法(C++中常叫做成员函数)。因为对我们来说,对象就是一个值或变量,并且拥有其方法,而方法是某种特定类型的函数。OOP就是使用方法来描述每个数据结构的属性和操作。
Go语言的方法声明和普通函数声明类似,只是在函数名前面多了一个参数。这个参数把这个方法绑定到这个参数对应的类型上。如下所示:
type Point struct {
X, Y float64
}
func (p *Point) Distance(q *Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
附加的参数称为方法的接收者,接收者的零值为nil。在Go语言中,接收者不适用特殊名(如C++的this或者Python的self),而是允许我们自主决定接收者的名字。由于方法会复制每一个实参变量,如果需要更新一个成员变量,或者如果一个实参太大而我们希望避免复制整个实参,通常使用指针来传递变量的地址,这也适用于更新接收者。
Go和许多其他面向对象的语言不同,Go允许将方法绑定到任何类型上,因此可以很方便地为简单的类型(数字、字符串、slice、map,甚至是函数等)定义附加的行为。同一个包下的任何类型都可以声明方法,只要它的类型既不是指针类型也不是接口类型。如下:
type Path []Point
func (path *Path) Distance float64 {
var sum float64
for i := range path {
if i > 0 {
sum += path[i-1].Distance(path[i])
}
}
return sum
}
嵌套结构体中的方法调用
嵌套结构体中,类型是外层结构体(或其指针)的接收者可以调用类型是内嵌结构体(或其指针)的方法。但不同于其他面向对象语言,类型是内层结构体的变量并不能转换为外层结构体类型,反之也不能。而在C++中基类指针被允许指向派生类对象。
import "image/color"
type ColoredPoint struct {
Point
Color color.RGBA
}
func main() {
p := ColoredPoint{Point{1, 1}, color.RGBA{255, 0, 0, 255}}
q := ColoredPoint{Point{2, 3}, color.RGBA{0, 0, 255, 255}}
fmt.Println(p.Distance(q.Point))
}
如果考虑具体实现,内嵌字段会告诉编译器生成额外的包装方法来调用Point声明的方法,这相当于以下代码:
func (p *ColoredPoint) Distance(q *Point) float64 {
return p.Point.Distance(q)
}
方法变量
方法也是函数,因此函数变量的概念也适用于方法。通常我们都在相同的表达式中使用和调用方法,但把两个操作分开也是可以的。如下所示,调用时只需要提供实参而不需要提供接收者就能够调用:
package main
import (
"fmt"
"math"
)
type Point struct {
X, Y float64
}
func (p *Point) Distance(q *Point) {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
func main() {
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance //方法变量
fmt.Printf("%T\n", distanceFromP) //输出 func(*main.Point)float64
fmt.Println(distanceFromP(&q)) //输出 5
}
如果包内的API调用一个函数值,并且使用者希望这个函数的行为是调用一个特定接收者的方法,方法变量就非常有用。
方法表达式
和调用一个函数不同,在调用方法时必须提供接收者,并且按照选择子的语法进行调用。而方法表达式写成T.f或者(*T).f,其中T是类型,是一种函数变量,把原来方法的接收者替换成函数的第一个形参,因此它可以像平常的函数一样调用。
p := Point{1, 2}
q := Point{4, 6}
distance := (*Point).Distance
fmt.Printf("%T\n%g\n", distance, distance(&p, &q))
//输出func(*main.Point,*main.Point) float64 和 5
如果你需要一个值来代表多个方法中的一个,而方法都属于同一个类型,方法表达式可以帮助你调用这个值所对应的方法来处理不同的接收者。
type Point struct {
X, Y float64
}
func (p Point) Add(q Point) Point {
return Point{p.X+q.X, p.Y+q.Y}
}
func (p Point) Sub(q Point) Point {
return Point{p.X-q.X, p.Y-q.Y}
}
type Path []Point
func (path Path) TranslateBy(offset Point, add bool) {
var op func(p, q Point) Point
if add {
op = Point.Add
} else {
op = Point.Sub
}
for i := range path {
path[i] = op(path[i], offset) //调用Add方法或Sub方法
}
}
上面代码中,Add方法和Sub方法的接收者、形参和返回值的类型都相同,因此可以借助方法表达式来统一表示一个变量类型,从而根据需要调用不同的方法。
封装
Go语言只有一种方式控制命名的可见性:定义的时候,首字母大写的标识符是可以从包中导出的,而首字母没有大写的则不可导出。同样的机制也适用于结构体内的字段和类型中的方法。因此在Go语言中要封装一个对象,必须使用结构体。
此外,在Go语言中封装的单元是包而不是类型。无论是在函数内的代码还是方法内的代码,结构体类型内的字段对于同一个包中的所有代码都是可见的。
本文部分内容摘自《Go程序设计语言》,有改动