英文:
Embedding vs. Inheritance in Go
问题
我正在尝试学习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
属性!
从这个错误中,我理解到嵌入不同于继承。
我的问题是:在Go语言中是否有类似于C++的结构体继承的等价方式,即一个函数可以接受一个基本结构体,或者Go语言处理这个问题的方式完全不同?在"Go way"中,我应该如何实现类似的功能?
英文:
I'm trying to learn Go, but I keep bashing my head against some of its concepts that apply differently compared to other languages.
Suppose I have a struct
type Vehicle struct {
Seats int
}
I now want another struct, which embeds Vehicle
:
type Car struct {
Vehicle
Color string
}
As I understand it, the Car
struct now embeds Vehicle
.
Now I want to have a function that takes any vehicle
func getSeats(v Vehicle){
return v.Seats
}
But whenever I try to pass a Car
:
getSeats(myCar)
I get the following error:
cannot use myCar (value of type Car) as Vehicle value in argument to getSeats
But my IDE tells me myCar
has a Seats
property!
From this I understand that embedding is not the same as inheriting.
My question is; Is there an equivalent to struct inheritance like c++, where a function can take a base struct, or is this something Go handles completely different? How would I implement something like this in the "Go way"?
答案1
得分: 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
}
// 自行车始终有1个座位
func (b *Bike) Seats() int {
return 1
}
type Motorcycle struct {
Vehicle
Sidecar bool
}
// 摩托车有基本座位数,如果有边车则再加1
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, // 在Vehicle中设置为2
},
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
方法,这就是为什么即使其Vehicle
的seats
值为2
,它仍然返回1
的原因。
在Motorcycle
的情况下,也调用了Motorcycle.Seats
方法,但在这里我们可以访问嵌入类型并仍然使用它来获得结果。
在Car
的情况下,调用的是Vehicle.Seats
方法,因为Car
没有“覆盖”Seats
方法。
英文:
Like you mentioned, Go has no inheritance in the typical sense. Embedding is really just syntactic sugar.
When embedding, you add a field to you struct with the exact same name as the type you are embedding. Any methods of the embedded struct can be called on the struct which embeds them, this does nothing more than forwarding them.
One tick is that, if the struct which embeds another already declares a method, it will be preferred over forwarding it, which allows you to sort of overwrite functions if you want to think of it like that.
As you have noticed, we can't use Car
as a Vehicle
even if Car
embeds Vehicle
since they are strictly not the same type. But any struct which embeds Vehicle
will have all methods which were defined by Vehicle
, thus if we define an interface which Vehicle
implements, all types that embed Vehicle
should also implement that interface.
For example:
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",
}))
}
This prints:
1
2
4
In the case of Bike
the Bike.Seats
method is called which is why it returns 1
even when the seats
value of its Vehicle
is 2
.
In the case of Motorcycle
the Motorcycle.Seats
method is also called, but here we can access the embedded type and still use it to get a result.
In the case of Car
, the Vehicle.Seats
method is called since Car
doesn't "overwrite" Seats
.
答案2
得分: 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等语言隐式使用的方式。
英文:
This is something I cared about when I started with Go. I thought using concepts from OOP languages like Java is natural way of doing things. Well its not, Go is not object oriented and it does not implement polymorphism trough inheritance. In the end, composition that Go supports takes good things from what inheritance offers and removes things that adds more overhead then effective help. Go's answer to polymorphism are interfaces. Go tries to be simple and you can usually achieve things only one obvious way. Lets say you have a Java classes like these:
class Base {
private int attribute;
public int getAttribute() {
return this.attribute;
}
}
class Something extends Base {}
In java every object is on the heap and you have pointer to it. It can be null
and it also means it has same size when you pass it around. This is one of the reasons why you can pass objects more freely (Whether you pass Base
or Something
, as long as argument type is super class, it will compile). Go is designed to manage memory more efficiently and use stack more then heap, and even heap is less fragmented. When you declare a struct
, the size of that struct is determined by data it holds, so you cannot pass it to places where embedded structure belongs. Let's give an example:
type Base struct {
attribute int32
}
func (b *Base) Attribute() int32 {
return b.attribute
}
type Something struct {
garbage int32
Base
}
While Base
has 4 bytes, Something
has 8, and attribute
has different offset. If the compiler lets you pass Something
in place of Base
, you would access garbage
instead of attribute
. If you want to get this behavior you have to use interface. Luckily, all you have to declare is this:
type BaseInterface interface {
Attribute() int32
}
Now that you have this interface, you can make general functions over all structs that embed Base (unless it also embeds something else that has method names Attribute
on the same level) like this:
func DoubleOfBaseAttribute(base BaseInterface) int32 {
return base.Attribute() * 2
}
This is also called Dynamic Dispatch and its what languages like C++ or Java use implicitly.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论