golang的继承机制

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{}
}

 

### Golang 中的继承概念与实现 在 Go 语言中,并不存在传统意义上的类(class),因此也没有经典面向对象编程中的继承机制。然而,Go 提供了一种称为 **组合(composition)** 的特性来替代继承[^1]。 #### 组合式继承 当一个结构体包含了另一个或多个匿名字段(这些字段本身也是结构体),这就形成了所谓的“组合”。这种设计模式允许外部结构体直接访问内部匿名结构体的方法和属性,从而模拟了多继承的效果[^3]。 例如,在给定的例子中: ```go type Goods struct { Name string Price float64 } type Book struct { Goods // 匿名字段, 表示Book拥有Goods的所有属性 Writer string } ``` 这里 `Book` 类型不仅具有自己的 `Writer` 属性,同时也具备来自 `Goods` 的所有成员变量(`Name`, `Price`)以及任何可能定义在其上的方法。这便是通过组合实现了类似于其他语言里的单重甚至多重继承的功能[^4]。 对于更复杂的场景,可以进一步嵌入更多层次或者不同类型的结构体以构建复杂的数据模型。需要注意的是,虽然这种方式看起来像是继承关系,但实际上它只是简单地把各个部分组装在一起而已;每个组件仍然是独立存在的实体[^2]。 #### 接口与多态 除了上述提到的基于结构体的组合之外,Go 还利用接口(interface)支持多态行为。只要某个类型满足特定接口的要求——即实现了该接口所规定的一组函数签名,则此类型就可以被当作那个接口类型处理。这样就达到了动态绑定的目的,而无需依赖于传统的继承体系。 ```go // 定义一个简单的接口 type Speaker interface { Speak() string } // Dog 结构体及其Speak方法实现 type Dog struct{} func (d *Dog) Speak() string { return "Woof!" } // Person 结构体也实现了同样的接口 type Person struct{} func (p *Person) Speak() string { return "Hello!" } ``` 在这个例子中,无论是 `*Dog` 或者 `*Person` 都能作为 `Speaker` 使用,因为它们都提供了符合预期的行为实现。这是另一种形式上接近继承的概念,但在实际操作层面更加灵活且易于维护。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值