在 Go 中嵌入而不是继承

Posted

技术标签:

【中文标题】在 Go 中嵌入而不是继承【英文标题】:Embedding instead of inheritance in Go 【发布时间】:2010-12-16 04:23:03 【问题描述】:

您对此设计决策有何看法?它有什么优点和缺点?

链接:

Embedding description

【问题讨论】:

【参考方案1】:

Gang of 4 的关键原则是“优先组合胜于继承”; Go 你跟随它;-)。

【讨论】:

继承被过度使用,我很欣赏 Go 如何简化组合,但我真正想知道的问题是嵌入是否可以完全取代继承。我想这是一个很难回答的问题,而无需实际编写一些代码 嗯,你不会(直接)得到一些关键的与继承相关的设计模式,比如模板方法,但这似乎不是一个杀手——在最坏的情况下,它似乎会失去一些便利(需要稍微更明确的编码)。 @Casebash:人们已经能够开始使用 JS 原型,我们可以说这只是一种组合。 仅仅因为他们不称之为继承并不意味着它不是。事实上,Go 的嵌入在功能上与多重继承相同。组合是使用多个对象协同工作,每个对象都有自己的问题域。 @Anthony:不,当你嵌入一个类型,并在嵌入对象中调用一个方法时,方法调用的接收者是嵌入对象,而不是完整对象。这意味着方法覆盖不会在没有一点努力的情况下工作(例如,使用您自己的包装器初始化嵌入对象)。【参考方案2】:

继承的唯一真正用途是:

多态性

Go 界面的“静态鸭子类型”系统解决了这个问题

从另一个类借用实现

这就是嵌入的目的

Go 的方法并不完全是一对一的映射,请考虑这个 Java 中继承和多态的经典示例 (based on this):

//roughly in Java (omitting lots of irrelevant details)
//WARNING: don't use at all, not even as a test

abstract class BankAccount

    int balance; //in cents
    void Deposit(int money)
    
        balance += money;
    

    void withdraw(int money)
    
        if(money > maxAllowedWithdrawl())
            throw new NotEnoughMoneyException();
        balance -= money;
    

    abstract int maxAllowedWithdrawl();


class Account extends BankAccount

    int maxAllowedWithdrawl()
    
        return balance;
    


class OverdraftAccount extends BankAccount

    int overdraft; //amount of negative money allowed

    int maxAllowedWithdrawl()
    
        return balance + overdraft;
    

这里,继承和多态是结合在一起的,如果不改变底层结构,你不能把它翻译成 Go。

我没有深入研究 Go,但我想它看起来像这样:

//roughly Go? .... no?
//for illustrative purposes only; not likely to compile
//
//WARNING: This is totally wrong; it's programming Java in Go

type Account interface 
    AddToBalance(int)
    MaxWithdraw() int


func Deposit(account Account, amount int) 
    account.AddToBalance(amount)


func Withdraw(account Account, amount int) error 
    if account.MaxWithdraw() < amount 
        return errors.New("Overdraft!")
    
    account.AddToBalance(-amount)
    return nil


type BankAccount 
    balance int


func (account *BankAccount) AddToBalance(amount int) 
    account.balance += amount;


type RegularAccount 
    *BankAccount


func (account *RegularAccount) MaxWithdraw() int 
    return account.balance //assuming it's allowed


type OverdraftAccount 
    *BankAccount
    overdraft int


func (account *OverdraftAccount) MaxWithdraw() int 
    return account.balance + account.overdraft

根据注释,这是一种完全错误的编码方式,因为在 Go 中使用 Java。如果要在 Go 中编写这样的东西,它的组织方式可能与此大不相同。

【讨论】:

您提到这不会编译,但有几点可以帮助其他阅读本文的人:类型需要 Go 中的类型文字。使用type RegularAccount struct 而不是type RegularAccount 您不能将func 原型放在类型定义中。在类型之外使用接收器语法:func (this *receiverType) funcName(parms) returnType 您必须为返回值的函数提供返回类型,例如func (account *RegularAccount) maxWithdraw() int 最后,在 Go 中,您需要以左大括号结束“func”行,而不是将其放在单独的行中。 我尝试将其编写为一个练习 - 对我来说在 Go 的早期阶段......我几乎可以使用它,如果有更多经验的人可以加入并纠正/完成它,我将不胜感激? gist.github.com/mindplay-dk/807179beda57e676b8fb【参考方案3】:

我刚刚开始学习 Go,但既然您要征求意见,我会根据我目前所知道的提供一个意见。嵌入似乎是 Go 中许多其他事物的典型特征,这是对已经在现有语言中完成的最佳实践的显式语言支持。例如,正如 Alex Martelli 所指出的,Gang of 4 说“更喜欢组合而不是继承”。 Go 不仅消除了继承,而且使组合比 C++/Java/C# 更容易、更强大。

我一直对诸如“Go 没有提供我在语言 X 中做不到的新东西”和“为什么我们需要另一种语言?”之类的 cmets 感到困惑。在我看来,从某种意义上说,Go 并没有提供任何以前通过某些工作无法完成的新事物,但在另一种意义上,Go 将促进和鼓励使用最好的技术已经在使用其他语言进行实践。

【讨论】:

在某些方面,Go 中的新东西就是被拿走的东西——这是一种新语言的关键原因。如果他们只是添加特性,那可能是 C++ ;) 但要去掉特性(继承、指针算法、手动内存分配)需要一种新语言。【参考方案4】:

在评论中,您想知道嵌入的想法是否足以“完全取代继承”。我会说这个问题的答案是“是”。几年前,我非常简单地使用了一个名为Snit 的 Tcl OO 系统,它使用组合和委托来排除继承。 Snit 仍然与 Go 的方法有很大不同,但在这一方面它们有一些共同的哲学基础。它是一种将功能和职责连接在一起的机制,而不是类的层次结构。

正如其他人所说,这实际上是关于语言设计者想要支持什么样的编程实践。所有这些选择都有各自的优缺点;我不认为“最佳实践”是一个必然适用于此的短语。我们可能最终会看到有人为 Go 开发一个继承层。

(对于任何熟悉 Tcl 的读者,我觉得 Snit 比 [incr Tcl] 更接近于语言的“感觉”。Tcl 完全是关于委托的,至少在我的思维方式中是这样。)

【讨论】:

【参考方案5】:

人们已经要求提供有关嵌入 Go 的信息的链接。

这是一个“Effective Go”文档,其中讨论了嵌入并提供了具体示例。

http://golang.org/doc/effective_go.html#embedding

当您已经很好地掌握了 Go 接口和类型时,该示例会更有意义,但是您可以通过将接口视为一组方法的名称来伪造它,并且如果您认为结构类似于C 结构体。

有关结构的更多信息,您可以查看 Go 语言规范,其中明确将结构的无名成员称为嵌入类型:

http://golang.org/ref/spec#Struct_types

到目前为止,我只是将它作为一种方便的方式来将一个结构放入另一个结构中,而不必为内部结构使用字段名称,因为字段名称不会为源代码添加任何值。在下面的编程练习中,我将一个提案类型绑定到一个具有提案和响应通道的类型中。

https://github.com/ecashin/go-getting/blob/master/bpaxos.go#L30

【讨论】:

【参考方案6】:

嵌入提供自动委托。这本身不足以取代继承,因为嵌入不提供任何形式的多态性。 Go 接口确实提供了多态性,它们与您可能使用的接口有点不同(有些人将它们比作鸭子类型或结构类型)。

在其他语言中,继承层次结构需要仔细设计,因为更改范围很广,因此很难做到。 Go 避免了这些陷阱,同时提供了强大的替代方案。

这是一篇深入探讨 Go 的 OOP 的文章:http://nathany.com/good

【讨论】:

【参考方案7】:

我喜欢。

您使用的语言会影响您的思维模式。 (只需让 C 程序员实现“字数统计”。他们可能会使用链表,然后切换到二叉树以提高性能。但是每个 Java/Ruby/Python 程序员都会使用字典/哈希。语言影响了他们的脑筋急转弯,他们想不出使用任何其他数据结构。)

使用继承,您必须构建——从抽象事物开始,然后将其子类化为细节。您实际有用的代码将被深埋在 N 级深中。这使得很难使用对象的“部分”,因为如果不拖入父类就无法重用代码。

在 Go 中,您可以通过这种方式“建模”您的类(使用接口)。但是你不能(不能)用这种方式编码。

相反,您可以使用嵌入。你的代码可以分解成小的、独立的模块,每个模块都有自己的数据。这使得重用变得微不足道。这种模块化与您的“大”对象几乎没有关系。 (即在 Go 中,您可以编写一个甚至不知道您的 Duck 类的“quack()”方法。但是在典型的 OOP 语言中,您不能声明“我的 Duck.quack() 实现不依赖于Duck 的任何其他方法。”)

在 Go 中,这不断迫使程序员考虑模块化。这导致程序耦合度低。低耦合使维护更容易。 (“哦,看,Duck.quack() 真的很长很复杂,但至少我知道它不依赖于 Duck 的其余部分。”)

【讨论】:

以上是关于在 Go 中嵌入而不是继承的主要内容,如果未能解决你的问题,请参考以下文章

Go 继承

Go 的继承

将 WireGuard 嵌入到 Windows 上的 Go 应用程序中

[GO专栏-2]Go语言的设计哲学

Golang 和继承

#yyds干货盘点#愚公系列2022年08月 Go教学课程 032-结构体方法继承