go语言中的接口

原文:https://medium.com/rungo/interfaces-in-go-ab1601159b3a
翻译:devabel
接口是golang中实现多态性的唯一好途径。

什么是接口?

我们在结构和方法课程中讨论了很多关于对象和行为的内容。 我们学习了结构体(以及其他非结构类型)实现方法。 接口是一组方法签名的集合,然后我们可以定义一个结构体实现该接口所有方法。因此,接口就是定义了对象的行为。
例如,结构体Dog可以walk和bark, 如果一个接口声明了walk和bark的方法签名,而Dog实现了walk和bark方法,那么Dog就实现了该接口。
接口的主要工作是仅提供由方法名称,输入参数和返回类型组成的方法签名集合。 由类型(例如struct结构体)来声明方法并实现它们。
如果您曾经是面向对象的程序员,您肯定会经常使用implements关键字来实现接口。 但是在go中,你没有明确提到一个类型是否实现了一个接口。 如果一个类型实现了在接口中定义的签名方法,则称该类型实现该接口。 就像说它像鸭子一样走路,像鸭子一样游泳,像鸭子一样嘎嘎叫,那就是鸭子。

定义接口

与struct类似,我们需要使用类型别名,通过interface关键字来简化接口声明。

type Shape interface {
    Area() float64
    Perimeter() float64
}

上面的代码中,我们定义了Shape接口,它有两个方法Area和Perimeter,他们不接收任何参数并返回float64。 任何实现这两个方法的类型我们都认为它实现了Shape接口。
由于interface也是一种类型,我们可以创建它的类型的变量。 在上面的例子中,我们可以创建一个类型为接口Shape的变量s。


1-yCE2tzPRap4Uc1Vn4iKHKQ.png

在我们对上面例子输出结果困惑前,让我解释一下。 接口有两种类型。 静态类型的接口是接口本身,例如上面的程序中的Shape。 接口没有静态值,而是指向动态值。 接口类型的变量可以保存实现接口的Type的值。 该类型的值成为接口的动态值,该类型成为接口的动态类型。
从上面的结果,我们可以看到接口的值是nil而且接口的类型也是nil。 这是因为此时,接口不知道是谁会实现它。 当我们使用带有接口参数的fmt包中的Println函数时,它指向接口的动态值,而Printf函数中的%T语法指的是接口的动态类型。 但实际上,接口的类型是Shape。

实现接口

让我们定义Shape接口提供的签名方法Area和Perimeter方法。 同时我们创建一个Shape结构体并使其实现Shape接口。


1-qqju8nevflb3D5-gDmpnBw.png

所以在上面的程序中,我们创建了Shape接口和矩形结构体类型Rect。 然后我们使用Rect接收器类型定义了Area和Perimeter等方法。 因此Rect实现了这些方法。 由于这些方法是由Shape接口定义的,因此Rect实现了Shape接口。
我们可以通过创建nil接口并为其指定Rect类型的结构来确认。 由于Rect实现了Shape接口,因此完全有效。 从上面的结果可以看出,s的动态类型现在是Rect,s的动态值是struct Rect的值,即{5 4}。 动态因为,我们可以为不同类型分配新的结构,也实现接口Shape。
有时,动态类型的接口也称为具体类型,因为当我们访问接口类型时,它返回它的基础动态值的类型,并且它的静态类型保持隐藏。
我们可以在s上调用Area方法,因为s的具体类型是Rect而Rect实现了Area方法。 我们还可以看到,我们可以将s与类型为Rect的r struct进行比较,因为它们都具有相同的Rect具体类型且具有相同的值。

让我们改变s的动态类型和值。
1-k8gbNmpSgw1zVwfnmig6uA.png

如果你阅读结构和方法课程,那么上面的程序不应该让你感到惊讶。 由于新的struct类型Circle也实现了Shape接口,我们可以为s分配一个Circle类型的结构。
现在我想你可以理解为什么接口的类型和价值是动态的。 就像我们在切片课中看到的那样切片保持对数组的引用,我们可以说通过动态保持对基础类型的引用,接口也以类似的方式工作。

你能猜出下面的程序会发生什么吗?
1-icnz60F1isROaYU-dqw7AA.png

在上面的程序中,我们删除了Perimeter方法。 上面的程序不会编译通过,编译器会抛出错误
program.go:22: cannot use Rect literal (type Rect) as type Shape in assignment:
        Rect does not implement Shape (missing Perimeter method)

从上面的错误中可以明显看出,为了成功实现接口,您需要实现接口声明的所有方法。

空接口

当接口没有方法时,它被称为空接口。 这由interface{}表示。 由于空接口没有任何方法,因此所有类型都实现了该接口。
您是否想知道fmt包中的Println函数如何接受在控制台上打印的不同类型的数据。 由于空接口,这是可能的。 让我们看看它是如何工作的。

让我们创建一个函数说明,它接受一个空接口并解释接口的动态值和类型。
1-FKuEaecQMtslenQ-VdJW1A.png

在上面的程序中,我们创建了一个自定义字符串类型MyString和一个struct类型Rect。 由于explain函数接受空接口,我们可以将MyString和Rect类型的变量传递给它,因为类型为空接口的参数i可以保存任何类型的值,因为所有类型都实现它。

多接口

一个类型可以实现多个接口。 我们来看一个例子吧。
1-sUynQOybf45TpWs7LDoNTg.png

在上面的程序中,我们使用Area方法创建了Shape接口,使用Volume方法创建了Object接口。 由于struct type Cube实现了这两种方法,因此它实现了这两种接口。 因此,我们可以将struct type Cube的值赋给Shape或Object类型的变量。
我们期望s具有c和o的动态值也具有c的动态值。 我们在Shape接口的s上使用了Area方法,因为它定义了Object接口类型的Area方法和Volume方法,因为它定义了Volume方法。 但是如果我们在o上使用Volume方法和在o上使用Area方法会发生什么。
让我们对上面的程序进行更改,看看会发生什么

fmt.Println("area of s of interface type Shape is", s.Volume())
fmt.Println("volume of o of interface type Object is", o.Area())

以上变化产生以下结果

program.go:31: s.Volume undefined (type Shape has no field or method Volume)
program.go:32: o.Area undefined (type Object has no field or method Area)

程序将无法编译,因为静态类型的s是Shape而o是Object。 为了使其工作,我们需要以某种方式提取这些接口的基础值。 这可以使用类型断言来完成。

类型断言

我们可以使用语法i.(Type)找出接口的基础动态值,其中i是接口,Type是实现接口i的类型。 Go将检查i的动态类型是否与Type相同。
因此,让我们重写前面的例子并提取接口的动态值。


1-Jsysj6iM4A-37XuhEvP98A.png

从上面的程序,我们现在可以访问变量c中接口s的底层值,它是Cube类型的结构。 现在,我们可以在c上使用Area和Volume方法。
在类型断言语法i.(Type)中,如果Type没有实现接口(类型)i那么go编译器会抛出错误。 但是如果Type实现了接口,但是我没有Type的具体值,那么go将在运行时出现混乱。 幸运的是,还有另一种类型断言语法的变体,即

value, ok := i.(Type)

在上面的语法中,我们可以检查使用ok变量,如果Type实现接口(类型)i,我有具体类型Type。 如果是,那么ok将为true,否则为false,value为struct的零值。
我还有一个问题。 我们如何知道接口的底层值是否实现了任何其他接口? 类型断言也可以这样做。 如果Type in断言语法是interface,那么go将检查i的动态类型是否实现接口Type。


1-K0TVqBr1WSnB1uuzCpbHyg.png

由于Cube结构不实现Skin接口,我们将ok2视为false,将value2视为nil。 如果我们使用更简单的v := i.(type)语法,那么我们的程序会报错

panic: interface conversion: main.Cube is not main.Skin: missing method Color

类型开关

我们已经看到空接口和它的使用。 让我们想一下使用解释函数的例子,如前所述。 由于explain函数的参数类型是空接口,我们可以将任何参数传递给它。 但是如果传递的参数是一个字符串,我们希望explain函数以大写形式打印它。 我们可以从字符串包中使用ToUpper函数,但由于它只接受字符串参数,我们需要确保内部解释函数中具体类型的空接口i是字符串。
这可以使用Type开关完成。 类型切换的语法类似于类型断言,它是i。(type)其中i是接口,type是固定关键字。 使用这个我们可以获得接口的具体类型而不是值。 但是这种语法只适用于switch语句。
我们来看一个例子吧


1-auPE-81-CFtMBuJktYnLog.png

在上面的程序中我们修改了解释函数以使用类型切换。 当使用任何类型调用explain函数时,我会收到其值并键入动态类型。 在switch中使用i.(type)语句,我们可以访问该动态类型。 使用switch中的case,我们可以做条件操作。 在字符串大小写的情况下,我们使用strings.ToUpper函数将字符串转换为大写。 但由于它只接受字符串类型,我们需要i的基础值,它是字符串类型,因此我们使用了类型断言。

嵌入式接口

在go中,接口不能实现其他接口或扩展它们,但我们可以通过合并两个或多个接口来创建新接口。 让我们重写我们的Shape-Cube程序。


1-UU13NxiyaYqNCE_t22YIxg.png

在上面的程序中,由于Cube实现了方法Area和Volume,它实现了Shape和Object接口。 但由于接口Material是这些接口的嵌入式接口,Cube也必须实现它。 发生这种情况是因为像匿名嵌套结构一样,嵌套接口的所有方法都被提升为父接口。

指针与值接收器

到目前为止,我们已经看到了带有值接收器 对于接受指针接收器的方法,接口是否正常。 我们来看看吧。


1-fPMsy7XAqXVPA2oZOl-1wQ.png

上面的程序不会编译而且会抛出编译错误

program.go:27: cannot use Rect literal (type Rect) as type Shape in assignment: Rect does not implement Shape (Area method has pointer receiver)

为什么会这样? 我们可以清楚地看到struct类型Rect正在实现接口Shape所声明的所有方法,那么为什么我们得到Rect不会实现Shape错误。 如果你仔细阅读错误,它说区域方法有指针接收器。 那么如果Area方法有指针接收器呢?
好吧,我们已经看到了结构课程,指针接收器的方法将对指针或值都起作用,如果我们在上面的程序中使用r.Area(),它就会编译得很好。
但是在接口的情况下,如果方法有指针接收器,那么接口将具有动态类型的指针而不是动态类型的值。 因此,当我们为接口变量分配类型值时,我们需要分配类型为value的指针。 让我们用这个概念重写上面的程序。


1-lj6zb6raHdkZn3tWBI2LGA.png

我们所做的唯一改变就是25行,用r的值代替,我们使用指向r的指针。 因此,s的具体值现在是一个指针。 以上程序将编译正常。

使用接口

我们已经学习了接口,我们看到它们可以采用不同的形式。 这就是多态性的定义。 接口在需要传递给它们的许多类型的参数的函数和方法的情况下非常有用,例如接受所有类型的值的Println函数。 如果你看到Println函数的语法,就像

func Println(a ...interface{}) (n int, err error)

这也是一种可变函数。
当多个类型实现相同的接口时,使用相同的代码可以很容易地使用它们。 因此,只要我们可以使用接口,我们就应该尽量使用它。