在Go语言中,嵌入和继承有何区别?

4

我正在尝试学习Go语言,但我总是遇到一些概念难以理解,这些概念与其他编程语言的应用方式不同。

假设我有一个结构体

type Vehicle struct {
    Seats int
}

我现在想要另一个结构体,它嵌入了Vehicle

type Car struct {
    Vehicle
    Color string
}

据我理解,Car 结构现在 嵌入Vehicle
现在我想要一个可以接受 任何 车辆的函数。
func getSeats(v Vehicle){
    return v.Seats
}

但是每当我尝试传递一个Car时:

    getSeats(myCar)

我遇到了如下错误:
cannot use myCar (value of type Car) as Vehicle value in argument to getSeats
但我的IDE告诉我myCar有一个Seats属性!从这里我理解嵌入不同于继承。
我的问题是:是否有相当于C++结构体继承的等效方式,可以使函数接受基本结构体?还是Go完全处理这个问题的方式不同?我该如何用"Go方式"实现这样的功能?

3
Go语言中没有继承。"我想要一个可以接受任何车辆的函数。" 这就是你的心智模型出现问题的地方。因为Vehicle是一个具体类型而不是接口,所以只有一个Vehicle类型。如果需要多态性,要么调用 getSeats(myCar.Vehicle),要么将Vehicle改为接口类型。请注意,不要改变原意。 - Peter
现在我想要一个函数,可以接受任何车辆作为参数。你真的需要这样做吗?这个例子是人为制造的,我很难理解这里的需求。 - user4466350
1
Go语言是独特的。你越早摒弃它与你所知道的其他编程语言相似的概念,你就能更快地理解它。花些时间理解这里的答案,其中包含了一些非常有价值的知识点。 - erik258
2个回答

5

像你提到的,Go语言没有传统意义上的继承。嵌入只是一种语法糖。

在嵌入时,你需要使用与要嵌入类型完全相同名称的字段添加到结构体中。任何嵌入的结构体方法都可以在嵌入它们的结构体上调用,这仅仅是将该方法转发过来而已。

有一个小技巧,如果嵌入另一个结构体的结构体已经声明了一个方法,那么它将优先于转发它,这使你可以对函数进行覆盖,如果你想这样考虑的话。

正如你所注意到的,即使Car嵌入了Vehicle,我们也不能将Car作为Vehicle使用,因为它们严格来说不是相同类型。但是,任何嵌入Vehicle的结构体都将拥有由Vehicle定义的所有方法,因此,如果我们定义一个Vehicle实现的接口,嵌入Vehicle的所有类型也应该实现该接口。

例如:

package main

import (
    "fmt"
)

type Seater interface {
    Seats() int
}

type Vehicle struct {
    seats int
}

func (v *Vehicle) Seats() int {
    return v.seats
}

type Car struct {
    Vehicle
    Color string
}

type Bike struct {
    Vehicle
    Flag bool
}

// A bike always has 1 seat
func (b *Bike) Seats() int {
    return 1
}

type Motorcycle struct {
    Vehicle
    Sidecar bool
}

// A motorcycle has the base amounts of seats, +1 if it has a side car
func (m *Motorcycle) Seats() int {
    return m.Vehicle.seats + 1
}

func getSeats(v Seater) int {
    return v.Seats()
}

func main() {
    fmt.Println(getSeats(&Bike{
        Vehicle: Vehicle{
            seats: 2, // Set to 2 in the Vehicle
        },
        Flag: true,
    }))

    fmt.Println(getSeats(&Motorcycle{
        Vehicle: Vehicle{
            seats: 1,
        },
        Sidecar: true,
    }))

    fmt.Println(getSeats(&Car{
        Vehicle: Vehicle{
            seats: 4,
        },
        Color: "blue",
    }))
}

这将打印:

1
2
4

Bike 中,调用了 Bike.Seats 方法,这就是为什么即使其Vehicleseats值为 2,它也会返回 1 的原因。
Motorcycle 中,同样调用了 Motorcycle.Seats 方法,但是我们可以访问嵌入类型并仍然使用它来获得结果。
Car 中,调用了 Vehicle.Seats 方法,因为 Car 没有 "覆盖" Seats

2

当我开始接触Go语言时,我很在意这个问题。我认为使用像Java这样的面向对象编程语言的概念是自然的做法。但实际上并非如此,Go不是面向对象的,并且它不通过继承来实现多态性。最终,Go支持的组合将继承提供的优点与添加更多开销而不是有效帮助的东西分离开来。Go对多态性的回答是接口。Go试图保持简单,通常只有一种明显的方法可以实现所需功能。假设你有这样的Java类:

class Base {
    private int attribute;

    public int getAttribute() {
        return this.attribute;
    }
}

class Something extends Base {}

在Java中,每个对象都在堆上并且您有指向它的指针。它可以是null,这也意味着在传递它时具有相同的大小。这是您可以更自由地传递对象的原因之一(无论您传递Base还是Something,只要参数类型是超类,它就会编译)。Go旨在更有效地管理内存并更多地使用堆栈,甚至堆也不那么碎片化。当您声明一个struct时,该结构的大小取决于它所持有的数据,因此您不能将其传递到嵌入式结构所属的位置。让我们举个例子:

type Base struct {
   attribute int32 
}

func (b *Base) Attribute() int32 {
    return b.attribute
}

type Something struct {
    garbage int32
    Base
}

虽然 Base 有4个字节,Something 有8个字节,并且 attribute 有不同的偏移量。如果编译器允许你将 Something 替换为 Base,那么你将访问到 garbage 而非 attribute。如果你想要获得此行为,你需要使用接口。幸运的是,你只需声明:

type BaseInterface interface {
    Attribute() int32
}

现在你有了这个接口,你可以对所有嵌入Base的结构体进行通用函数操作(除非它还嵌入了其他具有相同方法名为Attribute的东西)。例如:
func DoubleOfBaseAttribute(base BaseInterface) int32 {
    return base.Attribute() * 2
}

这也被称为动态派发,就像C++或Java这样的编程语言隐式使用它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接