登录
首页 >  Golang >  Go问答

在 Golang 中使用多个接口

来源:Golang技术栈

时间:2023-04-13 14:33:06 369浏览 收藏

小伙伴们有没有觉得学习Golang很有意思?有意思就对了!今天就给大家带来《在 Golang 中使用多个接口》,以下内容将会涉及到golang,若是在学习中对其中部分知识点有疑问,或许看了本文就能帮到你!

问题内容

我正在学习 Golang,作为使用接口的练习,我正在构建一个玩具程序。我在尝试使用“应该实现”两个接口的类型时遇到了一些问题——在 C++ 和 Java 中解决这个问题的一种方法是使用继承(还有其他技术,但我认为这是最常见的)。由于我在 Golang 中缺乏该机制,因此我不确定如何进行。下面是代码:

var (
    faces = []string{"Ace", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King"}

    suits = []string{"Hearts", "Diamonds", "Spades", "Clubs"}
)

type Card interface {
    GetFace() string
    GetSuit() string
}

type card struct {
    cardNum int
    face    string
    suit    string
}

func NewCard(num int) Card {
    newCard := card{
        cardNum: num,
        face:    faces[num%len(faces)],
        suit:    suits[num/len(faces)],
    }

    return &newCard
}

func (c *card) GetFace() string {
    return c.face
}

func (c *card) GetSuit() string {
    return c.suit
}

func (c *card) String() string {
    return fmt.Sprintf("%s%s ", c.GetFace(), c.GetSuit())
}

我想要达到的目标:

  • 我想隐藏我的结构类型,只导出接口,以便代码的客户端只使用 “卡” 接口
  • 我想要结构的字符串表示,因此使用 “String()” 方法实现接口,以便能够在我的结构的实例化上调用 “fmt.Println()”

当我尝试通过“卡”界面使用新卡并尝试获取字符串表示时,问题就出现了。我无法将接口作为 “String()” 方法实现的参数传递,因为存在与核心语言级别的接口可寻址性相关的编译器错误(仍在挖掘该文档)。一个非常简单的测试示例暴露了这个问题:

func TestString(t *testing.T) {
    card := NewCard(0)
    assert.EqualValues(t, "AceHearts ", card.String(), " newly created card's string repr should be 'AceHearts '")
}

编译器有充分的理由告诉我 "card.String undefined (type card has no field or method string)" 。我可以将 “String()” 方法添加到我的 “Card” 接口中,但我认为这并不干净:我有其他实体使用相同的模型实现,我必须在任何地方添加冗余;该方法已经有一个接口。

对于我遇到的上述问题,什么是好的解决方案?

编辑:( 解决一些非常好的评论)

  • 我不希望有另一个 Card 接口的实现;我不确定我是否理解我为什么要这样做,那就是改变界面
  • 我想让 Card 接口隐藏实现细节,并让客户端针对接口而不是具体类型进行编程
  • 我希望始终可以访问“card struct”实例化的所有客户端(包括通过 Card 接口实例化的客户端)的 String() 接口。我对只使用 String 接口的客户不感兴趣。在其他一些语言中,这可以通过实现两个接口来实现 - 多重继承。我不是说这是好是错,或者试图就此展开辩论,我只是在陈述一个事实!
  • 我的目的是找出该语言是否有任何机制可以同时满足这些要求。如果这是不可能的,或者从设计的角度来看问题应该以不同的方式解决,那么我已经准备好接受教育了
  • 类型断言非常冗长和明确,并且会暴露实现细节 - 它们有它们的位置,但我认为它们不适合我的情况

正确答案

我应该先回顾一些前言:

  • Go 中的接口与其他语言中的接口不同。你不应该假设来自其他语言的每个想法都应该自动转移。他们中的很多人都没有。
  • Go 既没有类也没有对象。
  • Go 不是 Java,Go 也不是 C++。它的类型系统与那些语言有显着和有意义的不同。

从你的问题:

我想让 Card 接口隐藏实现细节,并让客户端针对接口而不是具体类型进行编程

这是你其他问题的根源。

正如评论中提到的,我在多个其他包中看到了这一点,并将其视为一种特别讨厌的反模式。首先,我将解释为什么这种模式本质上是“反”的。

  • 首先,也是最相关的,你的例子证明了这一点。您采用了这种模式,它导致了不良影响。正如 mkopriva 所指出的,它产生了一个你 必须 解决的矛盾。
  • 接口的这种用法与其预期用途相反,并且您不会通过这样做获得任何好处。

接口是 Go 的多态机制。在参数中使用接口使您的代码更加通用。想想无处不在的io.Readerio.Writer。它们是出色的接口示例。它们是您可以将两个看似无关的库修补在一起并让它们正常工作的原因。例如,您可以记录到 stderr,或记录到磁盘文件,或记录到 http 响应。这些中的每一个都以完全相同的方式工作,因为log.New需要一个io.Writer参数,并且磁盘文件、stderr 和 http 响应编写器都实现了io.Writer. 仅仅使用接口来“隐藏实现细节”(我稍后会解释为什么这点失败),不会为您的代码增加任何灵活性。如果有的话,那就是滥用接口,将接口用于他们不打算完成的任务。

点/对位

  1. “通过确保隐藏所有细节,隐藏我的实现提供了更好的封装和安全性。”
  • 您没有实现更大的封装或安全性。通过使结构字段不导出(小写),您已经防止包的任何客户端弄乱结构的内部。包的客户端只能访问您导出的字段或方法。导出结构并隐藏每个字段并没有错。
  1. “结构值是肮脏和原始的,我对传递它们感觉不好。”
  • 然后不要传递结构,传递指向结构的指针。这就是你已经在这里做的事情。传递结构本身并没有错。如果您的类型表现得像一个可变对象,那么指向 struct 的指针可能是合适的。如果您的类型表现得更像一个不可变的数据点,那么 struct 可能是合适的。
  1. package.Struct“如果我的包导出,但客户必须始终使用,这不是很混乱*package.Struct吗?如果他们犯了错误怎么办?复制我的结构值是不安全的;事情会破坏!”
  • 为了防止出现问题,您实际上需要做的就是确保您的包 只返回 *package.Struct值。这就是你已经在这里做的事情。绝大多数时候,人们会使用 short assignment :=,所以他们不必担心类型是否正确。如果他们确实手动设置了类型,并且package.Struct意外选择了,那么他们在尝试为其分配 a 时会出现编译错误*package.Struct
  1. “它有助于将客户端代码与包代码解耦”
  • 可能是。但是,除非您有一个现实的期望,即您有多个这种类型的现有实现,那么这是一种过早优化的形式(是的,它确实会产生后果)。即使您的接口 确实有多个实现,这仍然不是您应该实际 返回 该接口的值的好理由。大多数情况下,只返回具体类型仍然更合适。要了解我的意思,请查看image标准库中的包。

什么时候真正有用?

制作过早的接口并 返回它 可能 对客户有帮助的主要现实案例是:

  • 你的包引入了接口的第二个实现
    • AND 客户端已在其 函数或类型中静态且显式(未:=)使用此数据类型 __
    • AND 客户也希望将这些类型或功能重用于新的实现。

请注意,即使您没有返回过早的接口,这也不会是一个破坏性的 API 更改,因为您只是 添加 了一个新的类型和构造函数。

如果您决定只 声明 这个过早的接口,并且仍然返回具体类型(如image包中所做的那样),那么客户可能需要做的所有补救措施就是花几分钟使用他们的 IDE 的重构工具来*package.Struct替换package.Interface.

它严重阻碍了包文档的可用性

Go 拥有一个名为 Godoc 的有用工具。Godoc 自动从源代码生成包的文档。当你在你的包中导出一个类型时,Godoc 会向你展示一些有用的东西:

  • 类型、该类型的所有导出方法以及返回该类型的所有函数都组织在 doc 索引中。
  • 类型及其每个方法在显示签名的页面中都有一个专用部分,以及解释其用法的注释。

一旦你将你的结构气泡包装到一个接口中,你的 Godoc 表示就会受到伤害。您的类型的方法不再显示在包索引中,因此包索引不再是包的准确概述,因为它缺少很多关键信息。此外,每种方法在页面上不再有自己的专用空间,这使得文档更难找到和阅读。最后也意味着你不再能够点击文档页面上的方法名来查看源代码。这也不是巧合,在许多采用这种模式的包中,这些不强调的方法通常没有文档注释,即使包的其余部分都有很好的文档说明。

在野外

https://pkg.go.dev/github.com/zserge/lorca

https://pkg.go.dev/github.com/googollee/go- socket.io

在这两种情况下,我们都看到了误导性的包概述,以及大多数接口方法未记录在案。

(请注意,我对这些开发人员中的任何一个都没有任何反对意见;显然每个包都有它的缺点,这些示例都是精心挑选的。我也不是说他们没有理由使用这种模式,只是他们的包文档受到了阻碍)

标准库中的示例

如果您对接口“打算如何使用”感到好奇,我建议您查看标准库的文档并注意接口的声明、作为参数和返回的位置。

https://golang.org/pkg/net/http/

https://golang.org/pkg/io/

https://golang.org/pkg/crypto/

https://golang.org/pkg/image/

这是我所知道的唯一一个可以与“接口隐藏”模式相媲美的标准库示例。在这种情况下,reflect 是一个非常复杂的包,内部有几种实现reflect.Type。另请注意,在这种情况下,即使有必要,也不会有人对此感到高兴,因为对客户的唯一真正影响是文档更加混乱。

https://golang.org/pkg/reflect/#Type

tl;博士

这种模式会损害您的文档,同时在此过程中不会完成任何事情,除了您可能会在非常特定的情况下稍微加快客户使用这种类型的并行实现的速度,您将来可能会或可能不会引入。

这些界面设计原则是为了客户的利益,对吧?设身处地为客户着想,问:我真正得到了什么?

到这里,我们也就讲完了《在 Golang 中使用多个接口》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于golang的知识点!

声明:本文转载于:Golang技术栈 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>