OOP: Inheritance in GOLANG complete guide

59 阅读6分钟

简介

我们将尝试使用与 JAVA 继承的比较来解释 GO 中的继承。首先我们要提到的是,GOLANG 没有 JAVA 中的“Extends”和“Implements”等关键字。 Go 确实以不同的方式提供了“Extends”和“Implements”关键字的有限功能,每种方式都有其自身的局限性。在我们继续了解 GO 中的继承之前,有一些要点值得一提。

  • Go 更喜欢组合而不是继承,它允许将结构嵌入到其他结构中。

  • Go 不支持类型继承。

我们将从 GO 中最简单的继承示例开始。然后我们将列出限制或缺失的功能。在进一步的迭代中,我们将修复限制或继续添加缺失的功能,直到我们编写出一个程序来显示 Go 中继承可能/不可能的所有属性。那么让我们开始吧

继承的最基本用例是子类型应该能够访问父类型的公共数据和方法。这是在 GO 中通过嵌入完成的。base 结构嵌入在 child 结构中,child 结构可以直接访问 base 结构的数据和方法。参见下面的代码:child 结构体可以直接访问数据 “color”,也可以直接调用函数 “say()”。

package main

import (
    "fmt"
)

type base struct {
    color string
}

func (b *base) say() {
    fmt.Println("hi from say function")
}

type child struct {
    base
    style string
}

func main() {
    base := base{Color: "Red"}
    child := &child{
        base: base,
        style: "somestyle",
    }
    
    child.say()
    fmt.Println("The color is " + child.color)
}

Output:

Hi from say function

The color is Red

上述程序的限制之一是您无法将 child 类型传递给需要基类型的函数,因为 GO 不允许类型继承。例如,下面的代码无法编译并给出错误 - cannot use child (type child) as type base in argument to check”

Program2

package main

import "fmt"

type base struct {
    color string
}

func (b *base) say() {
    fmt.Println("Hi from say function")
}

type child struct {
    base  //embedding
    style string
}

func check(b base) {
    b.say()
}

func main() {
    base := base{color: "Red"}
    child := &child{
        base:  base,
        style: "somestyle",
    }
    child.say()
    fmt.Println("The color is " + child.color)
    // cannot do this
    check(child)
}

上述错误基本上告诉我们,在 GO 中,仅仅使用嵌入是不可能实现子类型(sub-typing)的。让我们试着修复这个错误。这就是 GO 接口发挥作用的地方。请看下面的程序版本,除了上述功能外,它还修复了子类型化错误

Program3

package main

import (
    "fmt"
)

type iBase interface {
    say()
}

type base struct {
    color string
}

type child struct {
    base // embedding
    style string
}

func check(b iBase) {
    b.say()
}

func main() {
    base := base{color: "Red"}
    child := &child {
        base: base,
        stype: "somestyle",
    }
    child.say()
    fmt.Println("The color is " + child.color)
    check(child)
}

Hi from say function

The color is Red

Hi from say function

在上述程序中,我们

(a) 创建了一个接口 "iBase",其中有 "say "方法

(b) 我们修改了 "check "方法,使其接受 iBase 类型的参数

由于 base 结构实现了 "say "方法,而子结构反过来又嵌入了 base。因此,child 方法间接实现了 "say "方法,并成为 "iBase "的一个类型,这就是为什么我们现在可以将 child 传递给校验函数。

我们现在使用结构和接口的组合解决了一个限制,但还有一个限制。比方说,child 和 base 都有一个函数 "clear"。现在,"say" 方法会调用 "clear" 方法。

那么当使用 child 结构调用 "say" 方法时,"say" 方法将调用 base 结构的 "clear" 方法,而不是子结构的 "clear"方法。请看下面的示例

package main

import "fmt"

type iBase interface {
    say()
}

type base struct {
    color string
}

func (b *base) say() {
    b.clear()
}

func (b *base) clear() {
    fmt.Println("Clear from base's function")
}

type child struct {
    base  //embedding
    style string
}

func (b *child) clear() {
    fmt.Println("Clear from child's function")
}

func check(b iBase) {
    b.say()
}

func main() {
    base := base{color: "Red"}
    child := &child{
        base:  base,
        style: "somestyle",
    }
    child.say()
}

Clear from base's function

正如您在上面看到的,调用了 base 类的 “clear” 函数,而不是 child 类的 “clear” 方法。

这与 Java 不同,Java 中会调用 “child” 的 “clear” 方法。

解决上述问题的一种方法是将 "clear" 作为 base 结构中函数类型的属性。

这在 GO 中是可行的,因为函数在 GO 中是一等变量。请参见下面的解决方案

package main

import "fmt"

type iBase interface {
    say()
}

type base struct {
    color string
    clear func()
}

func (b *base) say() {
    b.clear()
}

type child struct {
    base  //embedding
    style string
}

func check(b iBase) {
    b.say()
}

func main() {
    base := base{color: "Red",
        clear: func() {
            fmt.Println("Clear from child's function")
        }}
    child := &child{
        base:  base,
        style: "somestyle",
    }
    child.say()
}

Clear from child's function

让我们尝试在上面的程序中添加一项功能:多重继承—— child 结构应该能够从两个基本结构访问多个属性和方法,并且子类型也应该是可能的。这是代码

package main

import "fmt"

type iBase1 interface {
    say()
}

type iBase2 interface {
    walk()
}

type base1 struct {
    color string
}

func (b *base1) say() {
    fmt.Println("Hi from say function")
}

type base2 struct {
}

func (b *base2) walk() {
    fmt.Println("Hi from walk function")
}

type child struct {
    base1 //embedding
    base2 //embedding
    style string
}

func (b *child) clear() {
    fmt.Println("Clear from child's function")
}

func check1(b iBase1) {
    b.say()
}

func check2(b iBase2) {
    b.walk()
}

func main() {
    base1 := base1{color: "Red"}
    base2 := base2{}
    
    child := &child{
        base1: base1,
        base2: base2,
        style: "somestyle",
    }
    
    child.say()
    child.walk()
    check1(child)
    check2(child)
}

Hi from say function

Hi from walk function

Hi from say function

Hi from walk function

在上面的程序中,child 嵌入了 base1 和 base2。它还可以作为 iBase1 和 iBase2 接口的实例分别传递给 check1 和 check2 函数,这就是我们实现多重继承的方式。

现在的一个大问题是,我们如何在 GO 中实现 "类型层次结构"。正如前面提到的,GO 不允许类型继承,因此也就不存在类型层次结构。

GO 故意不允许这一功能,因此接口行为的任何变化都只会传播到定义接口所有方法的直接结构中。

尽管我们可以使用接口和结构体实现类型层次结构,如下所示

package main

import "fmt"

type iAnimal interface {
    breathe()
}

type animal struct {
}

func (a *animal) breathe() {
    fmt.Println("Animal breate")
}

type iAquatic interface {
    iAnimal
    swim()
}

type aquatic struct {
    animal
}

func (a *aquatic) swim() {
    fmt.Println("Aquatic swim")
}

type iNonAquatic interface {
    iAnimal
    walk()
}

type nonAquatic struct {
    animal
}

func (a *nonAquatic) walk() {
    fmt.Println("Non-Aquatic walk")
}

type shark struct {
    aquatic
}

type lion struct {
    nonAquatic
}

func main() {
    shark := &shark{}
    checkAquatic(shark)
    checkAnimal(shark)
    lion := &lion{}
    checkNonAquatic(lion)
    checkAnimal(lion)
}

func checkAquatic(a iAquatic) {}
func checkNonAquatic(a iNonAquatic) {}
func checkAnimal(a iAnimal) {}

这是 go 创建类型层次结构的惯用方法,我们可以通过在结构级别和接口级别上使用嵌入来实现这一点。这里要注意的一点是,如果您希望在类型层次结构中进行区分,假设“shark”不应该同时是 “iAquatic”和 “iNonAquatic”,因此 shark 的方法集只有一个 swim 但没有 walk,所以 shark 只实现了接口 iAquatic 并未实现接口 iNonAquatic,而 lion 则是只实现了 walk 未实现 swim,通过方法集的差异就实现了类型层次的区分(因为在 go 中,只有实现接口的所有方法才算实现了该接口)。

iAnimal

iAquatic

shark

iNonAquatic

lion

总结

Go 不支持类型继承,但可以使用嵌入来实现相同的效果,但在创建这种类型层次结构时需要小心。

此外,Go 不提供方法重载。