前言
从之前的特性看下来,很难相信go竟然也是一个面向对象的语言,本篇来see see它的OOP特性,封装和组合体现在哪里~
1.方法声明
OOP的对象通常包含数据和行为,type定义的类型可以包含数据,其行为怎么表示呢?go是通过方法实现的。
go中的method方法与function函数非常类似,区别在于在函数名之前,方法携带了额外的参数(通常是所属类型type e.g. Point),这个参数将这个方法归属于这个参数的类型(表示是这个类型)。如下例,同样的距离方法,函数实现是两个点都在参数列表中,而方法将当前点p放在函数名前,而其它点放在参数列表中。
这个额外的参数(类似于Java中的隐含的this)是这个方法的接收者(receiver)。
// Point类型
type Point struct{ X, Y float64 }
// 包级函数
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// Point类型的方法
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", 函数调用
fmt.Println(p.Distance(q)) // "5", 方法调用
1.2 方法调用
如上代码,调用方法是将p.Distance(q),其中p对应额外参数(即接收者),而q对应声明中参数列表。调用方式与Java如出一辙。
1.3 优势
相对于函数(命名空间是整个包),由于每个类型的方法有着独立的命名空间,这样就不需要担心命名冲突,命名可以更简短;此外,由于调用方式是p.Distance,此处省略了包名。
2 指针接收者
由于go传参是值传递,类似于参数列表中如需修改变量或者太大拷贝浪费时需要传递其指针,接收者也是一样。如下例,声明时接收者是其类型的指针,调用时括号是必须,由于*和.的优先级问题。
// set方法 修改方法
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
// 调用
(*Point).ScaleBy
r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // "{2, 4}"
// go语法糖 隐式取地址&
p.ScaleBy(2)
Go中一个类型的接收者可以是其类型T、其指针*T,而调用时无论是T还是*T都是可以相互兼容的,这样可读性更高。只要简单理解指针的方法是修改方法,类型T的方法是查看方法,调用方式都可以一样。
声明接收者\调用者 | T p | *T pptr |
---|---|---|
T (p Point) Distance | ✔ p.Distance(q) | ✔(语法糖)隐式* 取值 pptr.Distance(q) = (*pptr).Distance(q) |
T* (p *Point) ScaleBy | ✔(语法糖) 隐式& 取地址 p.ScaleBy(2) = (&p).ScaleBy(2) | ✔ pptr.ScaleBy(2) |
3.组合
go的组合是通过结构体嵌套实现的,如下ColoredPoint类型嵌套了类型Point。
type Point struct{ X, Y float64 }
func (p Point) Distance(q Point) float64 {
dX := q.X - p.X
dY := q.Y - p.Y
return math.Sqrt(dX*dX + dY*dY)
}
type ColoredPoint struct {
Point // 嵌套
Color color.RGBA
}
// 匿名字段直接启用其子字段
var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // "1"
// 方法委托
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}
fmt.Println(p.Distance(q.Point)) // "5"
如下例,虽然声明形式像是Java中的组合,但是字段和方法的使用上,又像是继承的特性。
p.Distance(q.Point),其中p、q是ColoredPoint,p可以直接调用Point类型的方法,但是参数却不能直接传入q,
其实是因为Go编译器会根据嵌套类型的方法自动生成包装方法,委托给嵌套的Point执行。当一个类型T嵌入了另一个类型O,则T包含了O的所有方法,如果T有直接的方法,则出现复写(Override)的效果,编译器是先查找当前类型直接声明的方法,再查看嵌套类型的方法,最近原则,如果树深相同有两个类型相同的方法则编译报错(静态加载?)。
// Go编译器自动生成包装方法,委托给Point执行
func (p ColoredPoint) Distance(q Point) float64 {
return p.Point.Distance(q)
}
func (p *ColoredPoint) ScaleBy(factor float64) {
p.Point.ScaleBy(factor)
}
4.方法值
p.Distance返回了一个方法值,绑定了一个方法和特定接收值(类似于一个实例),可以直接用这个方法值只传入参数(后面的参数列表)即可调用。用图类似于函数值,可以传入API进行调用,与之不同的是可以指定一个接收者。
distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // "5"
4.1 方法表达式
类型T有函数表达式T.f 或者 (*T).f,返回的是一个函数,它的接收者在成为第一个参数。
distance := Point.Distance // method expression
fmt.Println(distance(p, q)) // "5"
方法表达式与方法值的区别如下:
引用 | 含义 |
---|---|
p.Distance | 方法值 类型:func(Point) float64 |
Point.Distance | 方法表达式,多了一个receiver参数 类型:func(Point,Point) float64 |
方法表达式的主要用途是当需要迭代或者其它有多个receiver,也有多种相同类型的方法需要选择。
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} }
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 {
// 迭代,多个receiver,多个Add(offset) Sub(offset)选择
path[i] = op(path[i], offset)
}
}
5.封装
封装的含义是指一个对象的字段方法对其客户端不可见,也叫做信息隐藏。之前有说过包级组件的可见性控制机制就是大小写,首字母大写导出包外,小写包装在包内。这个机制同样适用于struct或者type的字段和成员,也是包内外可见性的控制。PS:可见性控制的单元是
// 封装,虽然IntSet类型导出到了包外,但是其字段words没有,体现了封装
type IntSet struct {
words []uint64
}
// 反例,包外直接可以修改底层切片
type IntSet []uint64
- 客户端不能直接修改对象的字段
- 隐藏实现细节
- 不能随意set对象,防止破会不变式
Go的风格也是提供一些getter和setter,命名风格如下:
func (l *Logger) Flags() int
func (l *Logger) SetFlags(flag int)