golang也提供了继承机制,但采用组合的文法,因此称为匿名组合。与其他语言不同, golang很清晰地展示出类的内存布局是怎样的。
非指针方式组合
基本语法
// 基类
type Base struct {
// 成员变量
}
func (b *Base) 函数名(参数列表) (返回值列表) {
// 函数体
}
// 派生类
type Derived struct {
Base
// 成员变量
}
func (b *Derived) 函数名(参数列表) (返回值列表) {
// 函数体
}
继承规则
- 在派生类没有改写基类的成员方法时,相应的成员方法被继承。
- 派生类可以直接调用基类的成员方法,譬如基类有个成员方法为Base.Func(),那么Derived.Func()等同于Derived.Base.Func()
- 倘若派生类的成员方法名与基类的成员方法名相同,那么基类方法将被覆盖或叫隐藏,譬如基类和派生类都有成员方法Func(),那么Derived.Func()将只能调用派生类的Func()方法,如果要调用基类版本,可以通过Derived.Base.Func()来调用。
▪ 示例如下
package main
import "fmt"
type Base struct {
}
func (b *Base) Func1() {
fmt.Println("Base.Func1() was invoked!")
}
func (b *Base) Func2() {
fmt.Println("Base.Func2() was invoked!")
}
type Derived struct {
Base
}
func (d *Derived) Func2() {
fmt.Println("Derived.Func2() was invoked!")
}
func (d *Derived) Func3() {
fmt.Println("Derived.Func3() was invoked!")
}
func main() {
d := &Derived{}
d.Func1() // Base.Func1() was invoked!
d.Base.Func1() // Base.Func1() was invoked!
d.Func2() // Derived.Func2() was invoked!
d.Base.Func2() // Base.Func2() was invoked!
d.Func3() // Derived.Func3() was invoked!
}
内存布局
- golang很清晰地展示类的内存布局是怎样的,即Base的位置即基类成员展开的位置。
- golang还可以随心所欲地修改内存布局,即Base的位置可以出现在派生类的任何位置。
▪ 示例如下
package main
import "fmt"
type Base struct {
BaseName string
}
func (b *Base) PrintName() {
fmt.Println(b.BaseName)
}
type Derived struct {
DerivedName string
Base
}
func (d *Derived) PrintName() {
fmt.Println(d.DerivedName)
}
func main() {
d := &Derived{}
d.BaseName = "BaseStruct"
d.DerivedName = "DerivedStruct"
d.Base.PrintName() // BaseStruct
d.PrintName() // DerivedStruct
}
指针方式组合
基本语法
// 基类
type Base struct {
// 成员变量
}
func (b *Base) 函数名(参数列表) (返回值列表) {
// 函数体
}
// 派生类
type Derived struct {
*Base
// 成员变量
}
func (b *Derived) 函数名(参数列表) (返回值列表) {
// 函数体
}
继承规则
- 基类采用指针方式的组合,依然具有派生的效果,只是派生类创建实例的时候需要外部提供一个基类实例的指针。
- 其他规则与非指针方式组合一致。
▪ 示例如下
package main
import (
"fmt"
"log"
"os"
)
type MyJob struct {
Command string
*log.Logger
}
func (job *MyJob) Start() {
job.Println("job started!") // job.Logger.Println
fmt.Println(job.Command)
job.Println("job finished!") // job.Logger.Println
}
func main() {
logFile, err := os.OpenFile("./job.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0)
if err != nil {
fmt.Println("%s", err.Error())
return
}
defer logFile.Close()
logger := log.New(logFile, "[info]", log.Ldate|log.Ltime|log.Llongfile)
job := MyJob{"programming", logger}
job.Start()
job.Println("test finished!") // job.Logger.Println
}
在经过合适的赋值后,MyJob类型的所有成员方法可以很方便地借用所有log.Logger提供的方法。这对于MyJob的实现者来说,根本就不用意识到log.Logger类型的存在,这就是匿名组合的一个魅力所在。
内嵌类型不是基类
如果读者对基于类来实现的面向对象语言比较熟悉的话, 可能会倾向于将内嵌类型看作一个基类, 而外部类型看作其子类或者继承类, 或者将 外部类型 看作 "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。
一些总结
名字覆盖
上面说明了派生类成员方法名与基类成员方法名相同时基类方法将被覆盖的情况,这对于成员变量名来说,规则也是一致的。
package main
import "fmt"
type Base struct {
Name string
}
type Derived struct {
Base
Name string
}
func main() {
d := &Derived{}
d.Name = "Derived"
d.Base.Name = "Base"
fmt.Println(d.Name) // Derived
fmt.Println(d.Base.Name) // Base
}
名字冲突
匿名组合相当于以其类型名称(去掉包名部分)作为成员变量的名字。那么按此规则,类型中如果存在两个同名的成员,即使类型不同,但我们预期会收到编译错误。
package main
import "log"
type Logger struct {
Level int
}
type MyJob struct {
*Logger
Name string
*log.Logger // duplicate field Logger
}
func main() {
job := &MyJob{}
}