为什么Golang需要接口?

90
在Golang中,我们使用具有接收器方法的结构体。一切都很完美。
但我不确定接口是什么。我们在结构体中定义方法,如果要在结构体上实现方法,我们还需要再次编写该方法。
这意味着接口似乎只是方法定义,在页面上占用了额外的空间。
是否有任何示例可以解释为什么需要接口?

你将如何对未知结构的JSON进行反解析?如果没有 fmt.Printf,那么它将如何工作? - YOU
我想如果它不存在,它不应该工作。你是什么意思,它怎么工作?它是从 fmt 导出的。 - nikoss
可能是Go:interface{}的含义是什么?的重复问题。 - molivier
@molivier 这不是关于结构体是什么的问题,而是关于它们被用来做什么的问题,它们似乎完全没有用处。 - nikoss
看一下标准库中所有 io.Reader 和 io.Writer 的实例,这是一个很好的示例。 - JimB
1
为什么需要接口?解耦代码。请参见 https://dev59.com/RWXWa4cB1Zd3GeqPIweV#62297796。动态调用方法。请参见 https://dev59.com/xmw05IYBdhLWcg3wxUj_#62336440。访问Go包。请参见 https://dev59.com/fErSa4cB1Zd3GeqPVVA8#62278078。将任何值分配给变量。请参见 https://dev59.com/XWAg5IYBdhLWcg3w0Nsz#62337836。 - user12817546
7个回答

153

接口是一个太大的主题,无法在这里给出全面的答案,但有一些要点能使它们的使用更清晰。

接口是一种工具。你是否使用它们取决于你自己,但它们可以使代码更清晰,更短,更易读,并且它们可以提供良好的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说话时。我们能不能将它们都作为同一种类型实体来处理,例如动物?事实上并不行。当然,我们可以将它们都作为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{}参数的函数,并使用类型断言类型开关,这将减少辅助函数的数量,但仍然看起来非常丑陋。

解决方案?是的,使用接口。只需声明函数接受一个接口类型的值,该类型定义了您想要执行的行为,就这样:

func MakeTalk(s Sayer) {
    fmt.Println(reflect.TypeOf(s).Name(), "says:", s.Say())
}
你可以使用值为CatDogHorse或任何其他类型(直到现在还不知道的)具有Say()方法的对象进行函数调用。很酷。 在Go Playground上尝试这些示例。

2
我仍然无法理解为什么不直接将动物嵌入马、猫或狗中,这毫无意义。 - nikoss
@nikoss 我不确定我理解你的意思,请展示一下怎么将它们添加到切片中,然后循环并在每个上调用Say(),比如这样。 - icza
4
啊哈!Golang非常微妙,你的回答为我澄清了这个话题。 - Kenny Worden
3
完全一样。我有点理解什么是接口,但同时不理解其在实际例子中的用法。猫说话和狗叫的例子毫无意义。我正在尝试将此反映在现实世界的“客户,销售订单,提货订单等其他订单”上下文中,但不知道是否需要在那里使用接口。 - Dzintars
20
如果你在代码中看不到使用接口的好处,那么我的建议是现在不要创建/使用接口。当你开始重复编写处理多个具体类型的代码时,你会看到使用接口的好处和收益,届时你也可以随时添加它们。 - icza
显示剩余3条评论

20

接口提供了一些泛型功能。可以思考一下鸭子类型。

type Reader interface {
    Read()
}

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

type A struct {
}

func (_ A) Read() {
}

type B struct {
}

func (_ B) Read() {
}

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

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

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

2
很好的例子。此外 - 这与C++/Java不同 - 使用Go接口,您也不需要创建func callRead(r Reader)。这非常强大! - Evan Moran

10
正如已经提到的,接口是一种工具。并非所有的包都能从中受益,但对于某些编程任务来说,接口可以作为一个非常有用的抽象工具和创建包API的方式,特别适用于库代码或可能以多种方式实现的代码。
例如,考虑一个负责将一些原始图形绘制到屏幕上的包。我们可以认为屏幕的绝对基本要求是能够绘制像素、清除屏幕、定期刷新屏幕内容,以及获取关于屏幕当前尺寸等基本几何信息。因此,“屏幕”接口可能看起来像这样;
type Screen interface {
    Dimensions() (w uint32, h uint32)
    Origin() (x uint32, y uint32)
    Clear()
    Refresh()
    Draw(color Color, point Point)
}

现在我们的程序可能有几个不同的“图形驱动程序”,可以由我们的图形包使用来满足屏幕的基本要求。您可能正在使用某些本地操作系统驱动程序,也可能是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)

但现在您可以根据此创建一个屏幕类型变量;

var screen graphics.Screen = sdlGraphics

现在您有一个名为“screen”的通用“Screen”类型,它实现了(假设您已经编程)Clear(),Draw(),Refresh(),Origin()和Dimensions()方法。从此刻起,在您的代码中,您可以完全自信地发出如下命令:

screen.Clear()
screen.Refresh()

等等等等......这里的美妙之处在于你有一个标准类型叫做'Screen',其余的程序并不关心图形库的内部工作,可以使用它而无需考虑它。您可以信心十足地将'Screen'传递给任何函数等,它就会正常工作。

接口非常有用,它们真正帮助您思考代码的功能,而不是结构中的数据。而且小接口更好!

例如,不要在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

我认为 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" , 然后将被 "interface" 包装的 "struct" 返回。在这种情况下,只有方法才能访问 "struct" 字段。

2

我将在这里展示两个有趣的使用案例,涉及到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从字符串中读取,并使用io.MultiWriter并行写入到fileos.Stdout中,只需要使用io.Copy(w, r)。然后使用bytes.NewReader(slice)从切片中读取,并并行写入到fileos.Stdout中。接着,将切片复制到缓冲区中,使用io.Copy(buf, bytes.NewReader(slice)),然后使用file.Seek(0, 0)返回文件原点。然后先使用strings.NewReader从字符串中读取,然后继续使用io.MultiReader(r, file)bufio.NewScannerfile中读取所有内容,最后使用fmt.Println(scanner.Text())打印所有内容。

2- 下面是接口的另一个有趣用法:

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)
}

输出:

0
int:  1
int:  2
7

以下是需要翻译的内容:

并且有一个很好的例子可以参考:解释Go语言中的类型断言

还可以看看这个:Go语言:interface{}的含义是什么?


1
  1. 如果您需要实现一个方法,而不考虑结构体。

    您可以使用处理程序方法访问本地结构体,并在了解结构体之前使用处理程序。

  2. 如果您需要一种与其他或当前结构体不同的行为。

    您可能希望您的接口只包含少量方法,因为用户可能永远不会使用它们。 您可能希望根据用例将您的结构体分成不同的部分。

  3. 如果您需要实现任何类型。

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


1

什么是接口?

在Go语言中,接口是一种自定义类型,其他类型(接口)可以实现它。这是Go中实现抽象的主要机制之一。

接口通过描述其行为来描述类型。这是通过描述方法来展示它可以做什么来完成的:

在Playground中运行

package main

import "fmt"

// "Can Move" because has Move() method
type Animal interface {
    Move()
}

// Zebra is an "Animal" because can Move()
type Zebra struct {
    Iam string
}

func (z Zebra) Move() {
    fmt.Println("Zebra says: \"Animal\" can only move. I can move too, therefore I am an \"Animal\"!\n")
}

// "Can Move" because implements the "Animal" that can move
// "Can Swim" because has Swim() method
type Fish interface {
    Animal
    Swim()
}

// ClownFish is an "Animal" because can Move()
// ClownFish is a "Fish" because can Move() and Swim()
type ClownFish struct {
    Iam string
}

func (cf ClownFish) Move() {
    fmt.Println("ClownFish says: \"Animal\" can only move. I can move too, therefore I am an \"Animal\"!")
}

func (cf ClownFish) Swim() {
    fmt.Println("ClownFish says: \"Fish\" can move and swim. I can move and swim too, therefore I am a \"Fish\"!\n")
}

// "Can Move" because implements the "Fish" interface which implements an "Animal" interface that can move
// "Can Swim" because implements the "Fish" interface which can swim
// "Can Whistle" because has Whistle() method
type Dolphin interface {
    Fish
    Whistle()
}

// Orca is an "Animal" because can Move()
// Orca is a "Fish" because can Move() and Swim()
// Orca is a "Dolphin" because can Move(), Swim() and Whistle()
type Orca struct {
    Iam string
}

func (o Orca) Move() {
    fmt.Println("Orca says: \"Animal\" can only move. I can move too, therefore I am an \"Animal\"!")
}

func (o Orca) Swim() {
    fmt.Println("Orca says: \"Fish\" can move and swim. I can move and swim too, therefore I am a \"Fish\"!")
}

func (o Orca) Whistle() {
    fmt.Println("Orca says: \"Dolphin\" can move, swim and whistle. I can move, swim and whistle too, therefore I am a \"Dolphin\"!\n")
}

func main() {

    var pico Zebra = Zebra{Iam: "Zebra animal"}
    // pico can...
    pico.Move()

    var nemo ClownFish = ClownFish{Iam: "Clown fish"}
    // nemo can...
    nemo.Move()
    nemo.Swim()

    var luna Orca = Orca{Iam: "Orca dolphin"}
    // luna can...
    luna.Move()
    luna.Swim()
    luna.Whistle()

    // let's make slices with our "custom" types
    var anything []interface{}
    var animals []Animal
    var fishes []Fish
    var dolphins []Dolphin

    // we can add any type in "empty interface" type slice
    anything = append(anything, pico)
    anything = append(anything, nemo)
    anything = append(anything, luna)
    anything = append(anything, 5)
    anything = append(anything, "abcd")
    fmt.Printf("anything: %v\n", anything) // anything: [{Zebra animal} {Clown fish} {Orca dolphin} 5 abcd]

    // only Animal type can go here
    animals = append(animals, pico)
    animals = append(animals, nemo)
    animals = append(animals, luna)
    fmt.Printf("animals: %v\n", animals) // animals: [{Zebra animal} {Clown fish} {Orca dolphin}]

    // only Fish type can go here
    fishes = append(fishes, nemo)
    fishes = append(fishes, luna)
    fmt.Printf("fishes: %v\n", fishes) // fishes: [{Clown fish} {Orca dolphin}]

    // only Dolphin type can go here
    dolphins = append(dolphins, luna)
    // if you try to add a "Fish" to the slice of "Dolphin"s you will get an error:
    // cannot use nemo (variable of type ClownFish) as type Dolphin in argument to append: ClownFish does not implement Dolphin (missing Whistle method)
    // dolphins = append(dolphins, nemo)
    fmt.Printf("dolphins: %v\n", dolphins) // dolphins: [{Orca dolphin}]
}

正如您所看到的,这个接口帮助我们将斑马、小丑鱼和逆戟鲸类型添加到 []Animal 切片中。如果没有接口,这将是不可能的。简单来说,接口是一种自定义类型或工具,或者您想称之为什么都可以,它通过它们的行为将其他类型分组。

interface{}

空接口 interface{} 不指定任何方法或实现任何其他接口,这使它成为通用类型,因为所有其他类型都符合它。

正如您在上面代码片段中看到的,我们可以将任何类型推入 anything 切片中:

// we can add any type in "empty interface" type slice
anything = append(anything, pico)
anything = append(anything, nemo)
anything = append(anything, luna)
anything = append(anything, 5)
anything = append(anything, "abcd")
fmt.Printf("anything: %v\n", anything) // anything: [{Zebra animal} {Clown fish} {Orca dolphin} 5 abcd]

错误

  1. 在接口中添加使其臃肿或上下文特定的方法

在Playground中运行

    package main

    import "fmt"

    // isFish() method makes it context-specific
    // when we start using other animal types are we going to use isBird(), isCarnivore(), etc.?
    type Animal interface {
        Move()
        // isFish()
    }

    // Having a new interface or type which implements "Animal" is a
    // way better design because we can create other types as we need them
    type Fish interface {
        Animal
        FishSpecificBehavior()
    }

    type Salmon struct {
        name string
    }

    func (s Salmon) Move()                 {}
    func (s Salmon) FishSpecificBehavior() {}

    func main() {
        var f Fish = Salmon{"Salty"}
        fmt.Printf("fish is a Salmon by the name \"Salty\": %v\n", f) // fish is a salmon by the name "Salty": {Salty}
        fmt.Println("This is an extendable design!")
    }
  1. 在接口方法上使用指针接收器*T,但尽量将接口应用于实际实例T

在Playground中运行

    package main

    import "fmt"

    type I interface {
        a()
    }

    type T struct{}

    // in this case *T implements the interface "I", but not T
    func (t *T) a() {}

    type M struct{}

    // in this case M and *M both implement the interface "I"
    func (t M) a() {}

    func main() {
        var b I = &T{}
        // var b I = T{} // cannot use T{} (value of type T) as type I in variable declaration: T does not implement I (a method has pointer receiver)
        fmt.Printf("b: %v\n", b) // b: &{}

        var c1 I = &M{}
        var c2 I = M{}
        fmt.Printf("c1: %v c2: %v\n", c1, c2) // c1: &{} c2: &{}
    }

3. 构建深度嵌套的层级界面,而不是使用组合。 在 Playground 中运行
    package main

    import "fmt"

    // This is a bad design when we mimic inheritance and build a deeply nested structure
    type Animal interface {
        AnimalCan()
    }

    type Fish interface {
        Animal
        FishCan()
    }

    type SwordFish interface {
        Animal
        Fish
        SwordFishCan()
    }

    // This is a good design when we use composition and enforce a flatter structure
    type Movable interface {
        Move()
    }

    type Runnable interface {
        Run()
    }

    type Jumpable interface {
        Jump()
    }

    type Agile interface {
        Movable
        Runnable
        Jumpable
    }

    type Ant struct{}

    func (a Ant) Move() {}

    type Tigre struct{}

    func (t Tigre) Move() {}
    func (t Tigre) Run()  {}
    func (t Tigre) Jump() {}

    func main() {
        var ant Movable = Ant{}
        var tigre Agile = Tigre{}

        fmt.Printf("ant: %v tigre: %v", ant, tiger) // ant: {} tigre: {}
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接