Go也支持面向对象编程(OOP),在OOP中,一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些方法,而一个方法则是一个和特殊类型关联的函数。
1:方法声明
在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。
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),可以任意的选择接收器的名字。建议使用其类型的第一个字母,比如这里使用了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
对于一个给定的类型,其内部的方法都必须有唯一的方法名,但是不同的类型却可以有同样的方法名,每种类型都有其方法的命名空间,我们可以定义一个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和很多其它的面向对象的语言不太一样。
2:基于指针对象的方法
当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝,这种情况下我们就需要用到指针了。比如:
func (p Point) setX(x float64) { p.X = x } p := Point{1, 2} p.setX(3) fmt.Println(p) //{1 2}
这种情况下,可以用其指针而不是对象来声明方法,如下:
func (p *Point) ScaleBy(factor float64) { p.X *= factor p.Y *= factor }
这个方法的名字是 (*Point).ScaleBy 。这里的括号是必须的。想要调用指针类型方法 (*Point).ScaleBy ,只要提供一个Point类型的指针即可,像下面这样。
r := &Point{1, 2} r.ScaleBy(2) fmt.Println(*r) // "{2, 4}"
或者这样:
p := Point{1, 2} pptr := &p pptr.ScaleBy(2) fmt.Println(p) // "{2, 4}"
或者这样:
p := Point{1, 2} (&p).ScaleBy(2) fmt.Println(p) // "{2, 4}"
后面两种方法有些笨拙。幸运的是,go语言本身在这种地方会帮到我们。如果p是一个Point类型的变量,并且其方法需要一个Point指针作为接收器,go编译器允许用下面这种简短的写法: p.ScaleBy(2)
编译器会隐式地帮我们用&p去调用ScaleBy这个方法。这种简写方法只适用于“变量”,包括struct里的字段比如p.X,以及array和slice内的元素比如perim[0]。我们不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到:
Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal
另外,除了可以用一个 *Point 这样的接收器来调用Point的方法;编译器在这里也会给我们隐式地插入 * 这个操作符,所以下面这两种写法等价的:
pptr.Distance(q)
(*pptr).Distance(q)
因此,总结如下:
a:方法的接收器实际参数和其接收器的形式参数相同,比如两者都是类型T或者都是类型 *T :
Point{1, 2}.Distance(q) // Point
pptr.ScaleBy(2) // *Point
b:方法接收器形参是类型T,但接收器实参是类型 *T ,这种情况下编译器会隐式地为我们取变量的地址:
p.ScaleBy(2) // implicit (&p)
方法接收器形参是类型 *T ,实参是类型T。编译器会隐式地为我们解引用,取到指针指向的实际变量:
pptr.Distance(q) // implicit (*pptr)
如果类型T的所有方法都是用T类型自己来做接收器(而不是 *T),那么拷贝这种类型的实例就是安全的;调用他的任何一个方法也就会产生一个值的拷贝。但是如果一个方法使用指针作为接收器,你需要避免对其进行拷贝,因为这样可能会破坏掉该类型内部的不变性。比如你对bytes.Buffer对象进行了拷贝,那么可能会引起原始对象和拷贝对象只是别名而已,但实际上其指向的对象是一致的。紧接着对拷贝后的变量进行修改可能会有让你意外的结果。
在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的,比如下面这个例子:
type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type
3:通过嵌入结构体来扩展类型
之前介绍过,结构体中可以内嵌匿名成员,比如:
type Point struct{ X, Y float64 }
type ColoredPoint struct {
Point
Color color.RGBA
}
内嵌可以使我们在定义ColoredPoint时得到一种句法上的简写形式,并使其包含Point类型所具有的一切字段。
对于Point中的方法我们也有类似的用法,我们可以把ColoredPoint类型当作接收器来调用Point里的方法,即使ColoredPoint里没有声明这些方法:
red := color.RGBA{255, 0, 0, 255} blue := color.RGBA{0, 0, 255, 255} var p = ColoredPoint{Point{1, 1}, red} var q = ColoredPoint{Point{5, 4}, blue} fmt.Println(p.Distance(q.Point)) // "5" p.ScaleBy(2) q.ScaleBy(2) fmt.Println(p.Distance(q.Point)) // "10"
读者如果对基于类来实现面向对象的语言比较熟悉的话,可能会倾向于将Point看作一个基类,而ColoredPoint看作其子类或者继承类,或者将ColoredPoint看作"is a" Point类型。但这是错误的理解。请注意上面例子中对Distance方法的调用。Distance有一个参数是Point类型,但q并不是一个Point类,所以尽管q有着Point这个内嵌类型,我们也必须要显式地选择它。尝试直接传q的话你会看到下面这样的错误:
p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
一个ColoredPoint并不是一个Point,但他"has a" Point,并且它有从Point类里引入的Distance和ScaleBy方法。如果你喜欢从实现的角度来考虑问题,内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法,和下面的形式是等价的:
func (p ColoredPoint) Distance(q Point) float64 { return p.Point.Distance(q) } func (p *ColoredPoint) ScaleBy(factor float64) { p.Point.ScaleBy(factor) }
在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。
type ColoredPoint struct { *Point Color color.RGBA } p := ColoredPoint{&Point{1, 1}, red} q := ColoredPoint{&Point{5, 4}, blue} fmt.Println(p.Distance(*q.Point)) // "5" q.Point = p.Point // p and q now share the same Point p.ScaleBy(2) fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
一个struct类型也可能会有多个匿名字段。我们将ColoredPoint定义为下面这样:
type ColoredPoint struct {
Point
color.RGBA
}
然后这种类型的值便会拥有Point和RGBA类型的所有方法,以及直接定义在ColoredPoint中的方法。当编译器解析一个选择器到方法时,比如p.ScaleBy,它会首先去找直接定义在这个类型里的ScaleBy方法,然后找被ColoredPoint的内嵌字段们引入的方法,然后去找Point和RGBA的内嵌字段引入的方法,然后一直递归向下找。如果选择器有二义性的话编译器会报错,比如你在同一级里有两个同名的方法。
下面是一个小trick。这个例子展示了简单的cache,其使用两个包级别的变量来实现,一个mutex互斥量和它所操作的cache:
var ( mu sync.Mutex // guards mapping mapping = make(map[string]string) ) func Lookup(key string) string { mu.Lock() v := mapping[key] mu.Unlock() return v }
下面这个版本在功能上是一致的,但将两个包级吧的变量放在了cache这个struct一组内:
var cache = struct { sync.Mutex // guards mapping mapping map[string]string }{ mapping: make(map[string]string), } func Lookup(key string) string { cache.Lock() v := cache.mapping[key] cache.Unlock() return v }
4:方法值和方法表达式
执行方法通常是这样的形式:p.Distance(),实际上将其分成两步来执行也是可能的。p.Distance叫作“选择器”,选择器会返回一个方法"值":一个将方法(Point.Distance)绑定到特定接收器变量的函数。调用该函数时可以不需要指定接收器,只要传入函数的参数即可:
p := Point{1, 2} q := Point{4, 6} distanceFromP := p.Distance // method value fmt.Println(distanceFromP(q)) // "5" var origin Point // {0, 0} fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5) scaleP := p.ScaleBy // method value scaleP(2) // p becomes (2, 4) scaleP(3) // then (6, 12) scaleP(10) // then (60, 120)
和方法"值"相关的还有方法表达式。当调用一个方法时,与调用一个普通的函数相比,我们必须要用选择器(p.Distance)语法来指定方法的接收器。
当T是一个类型时,方法表达式可能会写作T.f或者(*T).f,会返回一个函数"值",这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:
p := Point{1, 2} q := Point{4, 6} distance := Point.Distance // method expression fmt.Println(distance(p, q)) // "5" fmt.Printf("%T ", distance) // "func(Point, Point) float64" scale := (*Point).ScaleBy scale(&p, 2) fmt.Println(p) // "{2 4}" fmt.Printf("%T ", scale) // "func(*Point, float64)"
例子如下:
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 { // Call either path[i].Add(offset) or path[i].Sub(offset). path[i] = op(path[i], offset) } }
5:封装
一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”。封装也是面向对象编程最关键的一个方面。
Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于struct或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个struct。
这种基于名字的手段使得在语言中最小的封装单元是package,而不是像其它语言一样的类型。一个struct类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。
下面的Counter类型允许调用方来增加counter变量的值,并且允许将这个值reset为0,但是在包外不允许随便设置这个值(因为压根就访问不到),包内则是可以的:
type Counter struct { n int } func (c *Counter) N() int { return c.n } func (c *Counter) Increment() { c.n++ } func (c *Counter) Reset() { c.n = 0 } func main() { var cc Counter fmt.Println(cc) // {0} cc.Reset() fmt.Println(cc) // {0} cc.Increment() cc.Increment() fmt.Println(cc) // {2} cc.n = 1 fmt.Println(cc.n) // 1 }