为什么 Golang 需要接口?

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

有什么例子可以解释我为什么需要一个接口吗?

30649 次浏览

I will show here, two interesting use cases of interfaces in Go:

1- See these two simple interfaces:

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


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

Using these two simple interfaces you may do this interesting magic:

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

Output:

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!!
!"#$%&'

I hope this code is clear enough:
reads from string using strings.NewReader and writes concurrently to both file and os.Stdout using io.MultiWriter with just io.Copy(w, r). Then reads from slice using bytes.NewReader(slice) and writes concurrently to both file and os.Stdout. Then copy slice to the buffer io.Copy(buf, bytes.NewReader(slice)) then goto the file origin using file.Seek(0, 0) then first read from string using strings.NewReader then continue reading that file using file2 and file3 and Print all of then using file4.


2- And this is another interesting use of interface:

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

output:

0
int:  1
int:  2
7

And nice example to see: Explain Type Assertions in Go

Also see: Go: What's the meaning of interface{}?

Interfaces are too big of a topic to give an all-depth answer here, but some things to make their use clear.

Interfaces are a tool. Whether you use them or not is up to you, but they can make code clearer, shorter, more readable, and they can provide a nice API between packages, or clients (users) and servers (providers).

Yes, you can create your own struct type, and you can "attach" methods to it, for example:

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

We can already see some repetition in the code above: when making both Cat and Dog say something. Can we handle both as the same kind of entity, as animal? Not really. Sure we could handle both as interface{}, but if we do so, we can't call their Say() method because a value of type interface{} does not define any methods.

There is some similarity in both of the above types: both have a method Say() with the same signature (parameters and result types). We can capture this with an interface:

type Sayer interface {
Say() string
}

The interface contains only the signatures of the methods, but not their implementation.

Note that in Go a type implicitly implements an interface if its method set is a superset of the interface. There is no declaration of the intent. What does this mean? Our previous Cat and Dog types already implement this Sayer interface even though this interface definition didn't even exist when we wrote them earlier, and we didn't touch them to mark them or something. They just do.

Interfaces specify behavior. A type that implements an interface means that type has all the methods the interface "prescribes".

Since both implement Sayer, we can handle both as a value of Sayer, they have this in common. See how we can handle both in unity:

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

(That reflect part is only to get the type name, don't make much of it as of now.)

The important part is that we could handle both Cat and Dog as the same kind (an interface type), and work with them / use them. If you were quickly on to create additional types with a Say() method, they could line up beside Cat and Dog:

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

Let's say you want to write other code that works with these types. A helper function:

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

Yes, the above function works with Cat and with nothing else. If you'd want something similar, you'd have to write it for each type. Needless to say how bad this is.

Yes, you could write it to take an argument of interface{}, and use type assertion or type switches, which would reduce the number of helper functions, but still looks really ugly.

The solution? Yes, interfaces. Simply declare the function to take a value of an interface type which defines the behavior you want to do with it, and that's all:

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

You can call this function with a value of Cat, Dog, Horse or any other type not known until now, that has a Say() method. Cool.

Try these examples on the Go Playground.

interface provide some kinds of generics. Think about duck typing.

type Reader interface{
Read()
}


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


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


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

It's ok to pass struct A, and B to callRead, because both implement Reader interface. But if without interface, we should write two function for A and B.

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


func callRead2(b B){
b.Read()
}
  1. if you need a method\s to be implemented regardless of the struct.

    you might have a handler method to access your local structs and use the handler before knowing the struct.

  2. if you need a behavior unique to other or current struct.

    you might want your interface be viewed with few methods because users might never use them. you might want your structs to be divided by its use cases.

  3. if you need a type that implements anything.

    you might know or not the type but at least you have the value.

As has already been stated, interfaces are a tool. Not all packages will benefit from them, but for certain programming tasks interfaces can be an extremely useful for abstraction and creating package APIs, and particularly for library code or code which may be implemented in more than one way.

Take for example a package which is responsible for drawing some primitive graphics to a screen. We can think of the absolute basic essential requirements of a screen as being able to draw a pixel, clear the screen, refresh the screen contents periodically, as well as get some basic geometric information about the screen such its current dimensions. Hence a 'Screen' interface might look like this;

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

Now our program might have several different "graphics drivers" which could be used by our graphics package to fulfill this basic requirement of a Screen. You might be using some native operating system driver, maybe the SDL2 package and maybe something else. And maybe in your program you need to support multiple options for drawing graphics because its dependent upon the OS environment and so on.

So you might then define three structs, each containing the required resources to the underlying screen drawing routines in the operating system / libraries etc;

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


type NativeDriver struct {
someDataField *Whatever
}


type AnotherDriver struct {
someDataField *Whatever
}

And then you implement in your code the method interface for all three of these structs so that any of these three structs can satisfy the requirements of the Screen interface

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

Now, your outside program really shouldn't care WHICH of these drivers you might be using, just so long as it can clear, draw and refresh the screen through a standard interface. This is abstraction. You provide at the package level the absolute minimum that is required for the rest of your program to work. Only code inside graphics needs to know all of the "nitty gritty" of HOW the operations work.

So you might know which screen driver you need to create for the given environment, maybe this is decided at the start of execution based on checking what's available on the users system. You decide that SDL2 is the best option and you create a new SDLGraphics instance;

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

But you can now create a type variable of Screen from this;

var screen graphics.Screen = sdlGraphics

And now you have a generic 'Screen' type called 'screen' which implements (assuming you programmed them) the Clear(), Draw(), Refresh(), Origin() and Dimensions() methods. From this point on in your code you can, in total confidence, issue statements such as

screen.Clear()
screen.Refresh()

And so on... The beauty of this is that you have a standard type called 'Screen' which the rest of your program, which really doesn't care about the inner workings of a graphics library, can use without having to think about it. You can pass around 'Screen' to any function etc in confidence that it will just work.

Interfaces are super useful and they really help you think about the function of your code rather than the data in your structs. And small interfaces are better!

For example instead of having a whole bunch of rendering operations inside the Screen interface, maybe you'll design a second interface like this;

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

It definitely takes some getting used to, depending on your programming experience and which languages you've used before. If you've been a strict python programmer up until now you'll find Go quite different, but if you've been using Java/C++ then you'll figure out Go pretty quickly. Interfaces give you object-oriented-ness without the annoyance that exists in other languages (e.g. Java).

Where I could see interface being useful is with implementing private struct fields. For example if you have this code:

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

Then o has access to all the struct fields. You may not want that. In that case you can use something like this:

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

The only change we made was adding the interface, then returning the struct wrapped in the interface. In this case only the methods will have access to the struct fields.