为什么Golang需要接口?

时间:2016-08-23 05:20:25

标签: go struct interface

在Golang中,我们使用带接收器方法的结构。一切都很完美到了。
但是,我不确定接口是什么。我们在结构中定义方法,如果我们想在结构上实现一个方法,我们无论如何都要在另一个结构下再写它 这意味着接口似乎只是方法定义,只占用了我们页面上额外不需要的空间。

有没有解释我为什么需要接口的例子?

6 个答案:

答案 0 :(得分:56)

界面太大了,不能在这里给出一个全面的答案,但有些事情可以让他们清楚地使用。

接口是工具。您是否使用它们取决于您,但它们可以使代码更清晰,并且它们可以在包或客户端(用户)和服务器(提供者)之间提供良好的API。

是的,您可以创建自己的struct类型,并且可以“附加”方法,例如:

type Cat struct{}

func (c Cat) Say() string { return "meow" }

type Dog struct{}

func (d Dog) Say() string { return "woof" }

func main() {
    c := Cat{}
    fmt.Println("Cat says:", c.Say())
    d := Dog{}
    fmt.Println("Dog says:", d.Say())
}

我们已经可以在上面的代码中看到一些重复:同时让CatDog说出来。我们可以像 animal 一样处理同一种实体吗?并不是的。我们当然可以同时处理interface{},但如果我们这样做,我们就无法调用他们的Say()方法,因为interface{}类型的值没有定义任何方法。

以上两种类型都有一些相似性:两者都有一个方法Say()具有相同的签名(参数和结果类型)。我们可以通过界面捕获

type Sayer interface {
    Say() string
}

界面仅包含方法的签名,但不包含实现

请注意,在Go中,类型隐式实现接口,如果其方法集是接口的超集。没有声明的声明。这是什么意思?我们以前的CatDog类型已经实现了这个Sayer接口,即使我们之前编写它们时这个接口定义甚至不存在,我们也没有触及它们来标记它们或者一些东西。他们就是这么做的。

接口指定行为。实现接口的类型意味着类型具有接口“规定”的所有方法。

由于两者都是Sayer,我们可以将它们作为Sayer的值处理,它们有共同点。看看我们如何能够统一处理:

animals := []Sayer{c, d}
for _, a := range animals {
    fmt.Println(reflect.TypeOf(a).Name(), "says:", a.Say())
}

(这反映了部分只是为了获取类型名称,现在不用多少。)

重要的是,我们可以同时处理CatDog(接口类型),并使用它们/使用它们。如果您使用Say()方法快速创建其他类型,则可以在CatDog旁边排列:

type Horse struct{}

func (h Horse) Say() string { return "neigh" }

animals = append(animals, Horse{})
for _, a := range animals {
    fmt.Println(reflect.TypeOf(a).Name(), "says:", a.Say())
}

假设您要编写适用于这些类型的其他代码。辅助函数:

func MakeCatTalk(c Cat) {
    fmt.Println("Cat says:", c.Say())
}

是的,上面的功能适用于Cat而没有别的。如果你想要类似的东西,你必须为每种类型写它。不用说这有多糟糕。

是的,你可以把它写成interface{}的参数,并使用type assertiontype switches,这会减少辅助函数的数量,但看起来仍然很难看。 / p>

解决方案?是的,接口。简单地声明函数取一个接口类型的值来定义你想用它做的行为,这就是全部:

func MakeTalk(s Sayer) {
    fmt.Println(reflect.TypeOf(s).Name(), "says:", s.Say())
}

您可以使用CatDogHorse或任何其他不知道的类型调用此函数,这种类型具有Say()方法。凉。

Go Playground上尝试这些示例。

答案 1 :(得分:5)

界面提供了一些种类的泛型。想想鸭子打字。

type Reader interface{
     Read()
}

func callRead(r Reader){
      r.Read()
}

type A struct{
}
func(_ A)Read(){
}

type B struct{
}
func(_ B)Read(){
}

将struct AB传递给callRead是可以的,因为它们都实现了Reader接口。 但是如果没有接口,我们应该为AB编写两个函数。

func callRead(a A){
     a.Read()
}

func callRead2(b B){
     b.Read()
}

答案 2 :(得分:3)

我将在这里展示Go中的两个有趣的接口用例:

1-请参阅以下两个简单的界面:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

使用这两个简单的界面你可以做到这个有趣的魔术:

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "io"
    "os"
    "strings"
)

func main() {
    file, err := os.Create("log.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    w := io.MultiWriter(file, os.Stdout)
    r := strings.NewReader("You'll see this string twice!!\n")
    io.Copy(w, r)

    slice := []byte{33, 34, 35, 36, 37, 38, 39, 10, 13}
    io.Copy(w, bytes.NewReader(slice)) // !"#$%&'

    buf := &bytes.Buffer{}
    io.Copy(buf, bytes.NewReader(slice))
    fmt.Println(buf.Bytes()) // [33 34 35 36 37 38 39 10 13]

    _, err = file.Seek(0, 0)
    if err != nil {
        panic(err)
    }

    r = strings.NewReader("Hello\nWorld\nThis\nis\nVery\nnice\nInterfacing.\n")
    rdr := io.MultiReader(r, file)
    scanner := bufio.NewScanner(rdr)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

输出:

You'll see this string twice!!
!"#$%&'

[33 34 35 36 37 38 39 10 13]
Hello
World
This
is
Very
nice
Interfacing.
You'll see this string twice!!
!"#$%&'

我希望这段代码足够清晰:
使用strings.NewReader从字符串中读取,并使用file仅使用os.Stdout同时写入io.MultiWriterio.Copy(w, r)。然后使用bytes.NewReader(slice)从切片读取并同时写入fileos.Stdout。然后将切片复制到缓冲区io.Copy(buf, bytes.NewReader(slice)),然后使用file.Seek(0, 0)转到文件来源,然后首先使用strings.NewReader从字符串中读取,然后继续使用file读取io.MultiReader(r, file)并{ {1}}并使用bufio.NewScanner打印所有内容。

2-这是界面的另一个有趣用途:

fmt.Println(scanner.Text())

输出:

package main

import "fmt"

func main() {
    i := show()
    fmt.Println(i) // 0

    i = show(1, 2, "AB", 'c', 'd', []int{1, 2, 3}, [...]int{1, 2})
    fmt.Println(i) // 7

}
func show(a ...interface{}) (count int) {
    for _, b := range a {
        if v, ok := b.(int); ok {
            fmt.Println("int: ", v)
        }
    }
    return len(a)
}

很好的例子:Explain Type Assertions in Go

另见:Go: What's the meaning of interface{}?

答案 3 :(得分:1)

  1. 无论结构如何,都需要实现方法。

    您可能有一个处理程序方法来访问您的本地结构并在知道该结构之前使用该处理程序。

  2. 如果您需要其他结构或当前结构所特有的行为。

    您可能希望用很少的方法查看界面,因为用户可能永远不会使用它们。 您可能希望将结构按其用例进行划分。

  3. 如果您需要一种可以实现任何内容的类型。

    您可能不知道类型,但至少您有值。

答案 4 :(得分:1)

如前所述,接口是一种工具。并非所有软件包都会从中受益,但是对于某些编程任务,接口对于抽象和创建软件包API尤其是库代码或可能以多种方式实现的代码非常有用。

例如,一个包负责将一些原始图形绘制到屏幕上。我们可以认为屏幕的绝对基本必要条件是能够绘制像素,清除屏幕,定期刷新屏幕内容以及获取有关屏幕的一些基本几何信息,例如其当前尺寸。因此,“屏幕”界面可能看起来像这样;

type Screen interface {
    Dimensions() (w uint32, h uint32)
    Origin() (x uint32, y uint32)
    Clear()
    Refresh()
    Draw(color Color, point Point)
}

现在我们的程序可能有几个不同的“图形驱动程序”,我们的图形包可以使用这些驱动程序来满足Screen的基本要求。您可能正在使用某些本机操作系统驱动程序,也许是SDL2软件包以及其他一些东西。也许在您的程序中,您需要支持用于绘制图形的多个选项,因为它取决于操作系统环境等。

因此,您然后可以定义三个结构,每个结构都包含操作系统/库等中的基础屏幕绘制例程所需的资源;

type SDLDriver struct {
    window *sdl.Window
    renderer *sdl.Renderer
}

type NativeDriver struct {
    someDataField *Whatever
}

type AnotherDriver struct {
    someDataField *Whatever
}

然后在代码中实现这三个结构的方法接口,以便这三个结构中的任何一个都可以满足Screen接口的要求

func (s SDLDriver) Dimensions() (w uint32, h uint32) {
    // implement Dimensions()
}

func (s SDLDriver) Origin() (x uint32, y uint32) {
    // implement Origin()
}

func (s SDLDriver) Clear() {
    // implement Clear()
}

func (s SDLDriver) Refresh() {
    // implement Refresh()
}

func (s SDLDriver) Draw(color Color, point Point) {
    // implement Draw()
}

...

func (s NativeDriver) Dimensions() (w uint32, h uint32) {
    // implement Dimensions()
}

func (s NativeDriver) Origin() (x uint32, y uint32) {
    // implement Origin()
}

func (s NativeDriver) Clear() {
    // implement Clear()
}

func (s NativeDriver) Refresh() {
    // implement Refresh()
}

func (s NativeDriver) Draw(color Color, point Point) {
    // implement Draw()
}

... and so on

现在,只要您可以通过标准界面清除,绘制和刷新屏幕,您的外部程序就不应该使用这些驱动程序。这是抽象的。您在程序包级别提供了其余程序正常工作所需的绝对最低要求。只有图形内部的代码才需要知道操作如何工作的所有“顽固性”。

因此,您可能知道需要为给定环境创建哪个屏幕驱动程序,也许这是在执行开始时根据检查用户系统上的可用内容决定的。您确定SDL2是最佳选择,然后创建一个新的SDLGraphics实例;

sdlGraphics, err := graphics.CreateSDLGraphics(0, 0, 800, 600)

但是您现在可以从中创建Screen的类型变量;

var screen graphics.Screen = sdlGraphics

现在,您有了一个称为“屏幕”的通用“屏幕”类型,该类型实现(假设您已对它们进行编程)Clear(),Draw(),Refresh(),Origin()和Dimensions()方法。从此刻开始,您可以完全放心地发出诸如

的语句
screen.Clear()
screen.Refresh()

以此类推...这样做的好处是您拥有一个称为“屏幕”的标准类型,该程序的其余部分实际上并不关心图形库的内部工作原理,因此无需使用它就可以使用。考虑一下。您可以将“屏幕”传递到任何功能等,以确保它可以正常工作。

接口非常有用,它们确实可以帮助您考虑代码的功能而不是结构中的数据。小接口更好!

例如,与其在Screen界面中没有一堆渲染操作,不如设计一个第二界面;

type Renderer interface {
    Fill(rect Rect, color Color)
    DrawLine(x float64, y float64, color Color)
    ... and so on
}

这确实需要一些时间来适应,这取决于您的编程经验和以前使用的语言。如果您一直是严格的python程序员,那么您会发现Go完全不同,但是如果您一直在使用Java / C ++,那么您很快就会发现Go。接口为您提供了面向对象的功能,而没有其他语言(例如Java)中存在的烦恼。

答案 5 :(得分:1)

我可以看到interface有用的地方是实现私有struct 领域。例如,如果您有以下代码:

package main
type Halloween struct {
   Day, Month string
}
func NewHalloween() Halloween {
   return Halloween { Month: "October", Day: "31" }
}
func (o Halloween) UK(Year string) string {
   return o.Day + " " + o.Month + " " + Year
}
func (o Halloween) US(Year string) string {
   return o.Month + " " + o.Day + " " + Year
}
func main() {
   o := NewHalloween()
   s_uk := o.UK("2020")
   s_us := o.US("2020")
   println(s_uk, s_us)
}

然后o可以访问所有struct字段。您可能不需要。在那里面 情况下,您可以使用以下内容:

type Country interface {
   UK(string) string
   US(string) string
}
func NewHalloween() Country {
   o := Halloween { Month: "October", Day: "31" }
   return Country(o)
}

我们所做的唯一更改是添加了interface,然后返回了struct 包裹在interface中。在这种情况下,只有方法可以访问 struct字段。