1

我最近喜欢上了 Go 编程语言,到目前为止我觉得它很棒,但我真的很难理解接口。我已经阅读了很多关于它们的内容,但它们对我来说仍然很抽象。

我写了一些使用下面接口的快速代码:

package main

import (
  "fmt"
  "math"
)

type Circer interface {
    Circ() float64
}

type Square struct {
    side float64
}

type Circle struct {
    diam, rad float64
}

func (s *Square) Circ() float64 {
    return s.side * 4
}

func (c *Circle) Circ() float64 {
    return c.diam * math.Pi
}

func (c *Circle) Area() float64 {
    if c.rad == 0 {
        var rad = c.diam / 2
        return (rad*rad) * math.Pi
    } else {
        return (c.rad*c.rad) * math.Pi
    }
}

func main() {

    var s = new(Square)
    var c = new(Circle)

    s.side = 2
    c.diam = 10

    var i Circer = s

    fmt.Println("Square Circ: ", i.Circ())

    i = c

    fmt.Println("Circle Circ: ", i.Circ())
}

我真的看不出Circer界面的目的。这些方法已经编写好了,我可以通过直接在结构上调用它们来节省两行代码,而不是使用 Circer 作为包装器。

有什么我想念的吗?我是否错误地使用了界面?任何帮助或示例表示赞赏。

4

3 回答 3

8

接口的重点是您可以制作如下所示的通用功能ShowMeTheCircumference

package main

import (
    "fmt"
    "math"
)

type Circer interface {
    Circ() float64
}

type Square struct {
    side float64
}

type Circle struct {
    diam, rad float64
}

func (s *Square) Circ() float64 {
    return s.side * 4
}

func (c *Circle) Circ() float64 {
    return c.diam * math.Pi
}

func ShowMeTheCircumference(name string, shape Circer) {
    fmt.Printf("Circumference of %s is %f\n", name, shape.Circ())
}

func main() {
    square := &Square{side: 2}
    circle := &Circle{diam: 10}
    ShowMeTheCircumference("square", square)
    ShowMeTheCircumference("circle", circle)

}

游乐场链接

于 2013-09-17T15:47:27.280 回答
4

您缺少的是您无法静态知道手头有什么样的东西的场景。让我们具体一点。

想想io.Reader,例如。有很多东西实现read了接口的方法。假设您编写了一个使用io.Reader. 例如,一个程序可能会打印一个io.Reader.

package mypackage

import (
    "fmt"
    "crypto/md5"
    "io"
    "strings"
)

func PrintHashsum(thing io.Reader) {
    hash := md5.New()
    io.Copy(hash, thing)
    fmt.Println("The hash sum is:", hash.Sum(nil))
}

并说您mypackage在其他地方的另一个文件中使用它:

func main() {
    mypackage.PrintHashsum(strings.NewReader("Hello world"))
}

现在假设您使用了一个io.Reader即时解压缩 zip 文件的实现,例如archive/zip包中的那个。

import "archive/zip"
// ... 
func main() {
    // ...
    anotherReader = zip.NewReader(...)
    // ...
}

由于接口的工作方式,您可以将这种 zip-sourced 阅读器输入 MD5-sum 计算mypackage.PrintHashsum功能,而无需对其现有代码进行任何其他操作或重新编译mypackage

func main() {
    // ...
    anotherReader = zip.NewReader(...)
    mypackage.PrintHashsum(anotherReader)
}

接口与让程序对动态扩展开放有关。在您的示例中,您可能会争辩说编译器应该确切地知道应该调用什么方法。但是在你的编译器为了速度而支持单独编译(如 Go)的情况下,编译器不可能知道:在编译mypackage时,编译器无法看到所有可能的实现io.Reader:它不是读心者或时间旅行者!

于 2013-09-17T17:47:41.353 回答
2

“我们要求严格界定怀疑和不确定的领域!” ——道格拉斯·亚当斯,《银河系漫游指南》

为了理解 Go 中的接口,我们必须首先了解我们为什么要对接口进行编程。

将什么与如何分开

我们使用接口来隐藏抽象背后的实现细节。我们喜欢隐藏这些细节,因为细节(即如何)比抽象更容易改变,并且因为它允许我们扩展和改变我们的应用程序,而不会在我们的程序中产生变化。当消费者依赖于接口而不是具体类型时,他们正在将他们的程序与接口背后的实现细节解耦,从而保护消费者免受更改,并使其更容易测试、扩展和维护他们的应用程序。

Golang 接口

Go 有一个非常强大的接口实现。与大多数语言一样,它提供了一种通过抽象来指定对象行为的方法,以便在使用抽象的任何地方都可以使用该抽象的任何实现,但在 Go 中,无需显式声明您的具体实现Go 中的给定接口会自动处理这个问题。

删除显式声明要求会产生有趣的后果,例如:您可以让程序在执行过程中显示接口,以帮助您识别适当的抽象,而无需在发现所有实现时对其进行注释。这也意味着为测试创建的接口不需要污染您的实现代码。此外,接口和实现者之间没有明确的关系,因此实现者在那个方向上没有依赖/耦合。

Circer 接口示例

在您提供的示例中,避免使用接口而不是绑定到实现(具体化)的复杂性和“认知负荷”当然更简单、更容易。在大多数琐碎的例子中,使用接口似乎是教条而不是工程。

概括

接口是我们解耦应用程序以使其更容易随着时间的推移而增长的强大方式。如果您预计会发生变化/变化(并且需要保护您的应用程序免受这种变化/变化的影响),那么创建并依赖于接口是朝着正确方向迈出的一步。

对于更多...

请参阅这篇“不错的” Go Object Oriented Design 帖子

并查看SOLID 设计原则,因为在考虑抽象的含义以及管理依赖项和更改时,它是一个很好的起点。

于 2013-09-17T19:13:38.170 回答