"Go语言的面向对象机制与一般语言不同。 它没有类层次结构, 甚至可以说没有类; 仅仅通过组合( 而不是继承) 简单的对象来构建复杂的对象。" -- 《Go语言圣经》
1.匿名组合
1.1 匿名组合定义
golang中组合语法,就是在一个类中,引入了另一个类,如
type Logger struct{
}
type Work struct{
log Logger
}
type Work2 struct{
log *Logger
}
func (Logger)Info(v ...interface{}){
}
如上边代码所示,Work类中定义了一个Logger类型的变量,这种是比较常见的引入方式,姑且在此称之为非匿名组合,那什么是匿名组合呢,如其名,就是在组合的过程中,不给名字呗,如代码所示:
type Logger struct {
}
type Work struct {
Logger
}
type Work2 struct {
*Logger
}
func (Logger) Info(v ...interface{}) {
}
上边的代码中,Work类与Work2类均与Logger类匿名组合。两个类唯一不同的是,Work2中组合的是指针类型的Logger类。
1.2 组合对象初始化
非匿名组合初始化方式
func main(){
var wk = Work{log:Logger{}}
var wwk = Work{Logger{}}
//...and so on
var wk2 = Work2{log:new(Logger)}
var wwk2 = Work2{new(Logger)}
//... and so on
}
匿名组合初始化
func main(){
var wk = Work{Logger{}}
var wwk = Work{Logger:Logger{}}
//... and so on
var wk2 = Work2{new(Logger)}
var wwk2 = Work2{Logger:&Logger{}}
//... and so on
}
上边是匿名组合常见的初始化方式。匿名组合后,被包含类得方法和属性可以直接被使用,即使是私有变量。
注意事项:
1.匿名组合多个类时,不同的类存在相同的方法,会不会冲突?答案是,不同的类中,不同的方法时不会冲突的,但是在调用这个方法时,需要明确是那个类中的方法,如果匿名组合进来的类得方法,与这个类主体中的方法发生冲突,那么默认情况下,会使用主体类中的方法。
2.匿名组合多个类时,类名相同,会不会冲突?答案是,会。就算包名不同,类名相同,也会冲突。
示例代码:
package main
import(
"bufio"
)
type Reader struct {
}
type Work4 struct {
Reader
bufio.Reader
}
上边代码编译时,会提示Reader重复定义 duplicate field Reader
原因在于,匿名组合中,没有给引入的类命名,所以默认采用了类名作为属性名。如上边wwk2这个对象在调用Logger的Info方法时,可以采用wwk2.Info(“hello”),也可以采用wwk2.Logger.Info(“hello”).
下边附上一段完整的演示代码,注意会报错哦,这段代码包含了上边的duplicate field Reader错误:
package main
import (
"bufio"
"fmt"
)
type Logger struct {
}
type Work struct {
Logger
}
type Work2 struct {
*Logger
}
type Work3 struct {
log *Logger
}
type Reader struct {
}
type Work4 struct {
Reader
bufio.Reader
}
func (Logger) Info(v ...interface{}) {
fmt.Println(v...)
}
func main() {
var wk = Work{Logger{}}
wk.Info("hello: Work{Logger{}}")
var wwk = Work{Logger: Logger{}}
wwk.Info("hello: Work{Logger: Logger{}}")
//... and so on
var wk2 = Work2{new(Logger)}
wk2.Info("hello: Work2{new(Logger)}")
var wwk2 = Work2{Logger: &Logger{}}
wwk2.Info("hello: Work2{Logger: &Logger{}}")
wwk2.Logger.Info("hello: wwk2.Logger.Info")
var wk3 = Work3{new(Logger)}
wk3.log.Info("hello: Work3{new(Logger)}")
}
3. 结构体嵌入和匿名成员
Go语言提供别样的 结构体嵌入 机制,让一个结构体包含另一个结构体类型的 匿名成员 , 这样就可以通过简单的点运算符x.f来访问匿名成员链中嵌套的x.d.e.f成员。
Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字; 这类成员就叫匿名成员。 匿名成员的数据类型必须是命名的(而不是匿名的)类型或指向一个命名的类型的指针。
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
由于有了匿名嵌入的特性, 我们可以直接访问内嵌类型的成员变量而不需要给出完整的路径:
var w Wheel
w.X = 8 // 等价于 w.Circle.Point.X = 8
w.Y = 8 // 等价于 w.Circle.Point.Y = 8
w.Radius = 5 // 等价于 w.Circle.Radius = 5
w.Spokes = 20
同样的规则,内嵌类型的方法也会提升为外部类型的方法。
3.1 匿名冲突(duplicate field)
匿名成员也有一个隐式的名字,以其类型名称(去掉包名部分)作为成员变量的名字。 因此不能同一级同时包含两个类型相同的匿名成员, 这会导致名字冲突。
type Logger struct {
Level int
}
type MyJob struct {
*Logger
Name string
*log.Logger // duplicate field Logger
}
4. 匿名组合不是继承
4.1 方法的接受者没变
当我们嵌入一个类型,这个类型的方法就变成了外部类型的方法,但是当它被调用时,方法的接受者是内部类型(嵌入类型),而非外部类型。— Effective Go
type Job struct {
Command string
*log.Logger
}
func (job *Job)Start() {
job.Log("starting now...")
... // 做一些事情
job.Log("started.")
}
上面这个Job例子,即使组合后调用的方式变成了job.Log(...),但Log函数的接收者仍然是 log.Logger指针,因此在Log中也不可能访问到job的其他成员方法和变量。
4.1 内嵌类型不是基类
如果读者对基于 类 来实现的面向对象语言比较熟悉的话, 可能会倾向于将 内嵌类型 看作一个基类, 而 外部类型 看作其子类或者继承类, 或者将 外部类型 看作 "is a" 内嵌类型 。 但这样理解是错误的。
type Point struct{ X, Y float64 }
type ColoredPoint struct {
Point
Color color.RGBA
}
func (p Point) Distance(q Point) float64 {
dX := q.X - p.X
dY := q.Y - p.Y
return math.Sqrt(dX*dX + dY*dY)
}
请注意上面例子中对Distance方法的调用。 Distance有一个参数是Point类型, 但q并不是一个Point类, 所以尽管q有着Point这个内嵌类型, 我们也必须要显式地选择它。 尝试直接传q的话你会看到错误:
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.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
一个ColoredPoint并不是一个Point, 但ColoredPoint "has a" Point, 并且它有从Point类里引入的 Distance方法。
实际上,从实现的角度来考虑问题, 内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法, 和下面的形式是等价的:
func (p ColoredPoint) Distance(q Point) float64 {
return p.Point.Distance(q)
}
当Point.Distance被以上编译器生成的包装方法调用时, 它的接收器值是p.Point, 而不是p。
4.3 匿名冲突(duplicate field) 和隐式名字
匿名成员也有一个隐式的名字,以其类型名称(去掉包名部分)作为成员变量的名字。 因此不能同一级同时包含两个类型相同的匿名成员, 这会导致名字冲突。
type Logger struct {
Level int
}
type MyJob struct {
*Logger
Name string
*log.Logger // duplicate field Logger
}
以下两点都间接说明匿名组合不是继承:
匿名成员有隐式的名字 匿名可能冲突(duplicate field)