[奔跑的 Go] 教程十四、 Go 语言接口(Interface)

Golang

接口的定义是非常棒的,而且也是实现 Go 中多态性的唯一途径。

☛ 什么是接口?

我们在结构和方法课程上讨论了许多关于 对象行为 的内容。我们看到了结构(以及其他非结构类型)实现方法。interface方法签名 的集合,可以由 对象 来实现。因此,接口定义了对象的行为。

例如,Dogwalkbark 。如果一个接口声明了 walkbark 的方法签名,而 Dog 实现了 walkbark 方法,那么 Dog 将实现该接口。

接口的主要工作是只提供方法名、输入参数和返回类型组成的方法签名。方法的声明和实现取决于类型(如结构体类型)。

如果原来你是面向对象(OOP)编程,那么你可能在实现接口时经常使用 implement 关键字。但是在 Go 中,你并没有显式的提到类型是否实现接口。如果类型实现接口中定义的签名方法,则该类型就实现了该接口。比如说,如果它像鸭子一样走路,像鸭子一样游泳和像鸭子一样叫,那么它就是鸭子

☛ 声明接口

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

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

在上面,我们定义了 Shape 接口,它有两个方法 AreaPerimeter ,这两个方法不接受参数并返回 float64,任何实现这些方法的类型都称为实现 Shape 接口。

因为 interface 是一种类型,我们可以创建它类型的变量。在上述情况下,我们可以创建一个变量 s 的类型接口 Shape

https://play.golang.org/p/oGRDKbrEJYb

在我们因上述结果而头痛之前,让我解释一下,接口有两种类型。接口的 static type 是接口本身,例如上面程序中的 Shape 。一个接口没有 static value ,而是指向 dynamic value ,接口类型的变量可以保存实现接口的类型的值 。该类型的值成为接口的 dynamic type,该类型成为接口的 dynamic type

从上面的结果可以看出, zero 值和接口类型为 nil这是因为此时,接口还不知道是谁在实现它。当我们使用带有接口参数的 fmt 包中的 Println 函数时,它指向接口的动态值,Printf 函数中的 %T 语法是指接口的动态类型。但是实际上,接口的类型 sShape

☛ 实现接口

根据接口 Shape 的方法签名定义 Area 和 Perimeter ,然后新建一个Rect类型的结构体来实现 Shape接口 。

https://play.golang.org/p/Hb__pA7Xp5V

在上面的程序中,创建了接口 Shape和结构体类型Rect,然后以 Rect为接收器定义了方法 Area 和 Perimeter ,因此Rect 实现了这两个方法。由于这两个方法是由接口Shape定义的,所以Rect 实现了Shape.

创建一个初始值为nil的接口 s ,将类型为 Rect的结构体赋值给它。因为Rect 实现了 Shape接口,所以这么做完全正确。上例还可得知,动态类型s现在是 Rect ,且s的值是Rect 的值,即{5 4}。之所以称之为动态,是因为可以为它分配任何实现了Shape的结构体。

有时,动态类型的接口也称为 **具体类型(concrete type)** ,当访问接口的类型时,它返回底层动态值的类型,而其静态类型是隐藏的。

可以在 s上调用 Area方法,因为 s的具体类型是 Rect,而Rect实现了Area方法。此外,结构体类型为Rects和'r可以进行比较且相等,是因为两者有相同的具体类型Rect和相同的值。

下面来改变s动态类型和值 

https://play.golang.org/p/K8mBGpfApjJ

如果你看过关于 结构体(structs) 和 方法(methods)的课程,那么上面的程序应该不会让你感到惊讶。由于新的结构体类型 Circle 也实现了接口 Shape ,所以能将Circle类型的结构体赋值给 s

现在你应该理解为什么接口的类型和值是动态的了。正如在切片(slices) 一课中看到的,一个切片保存了对array的引用,可以说interface 也是类似的工作方式,动态得保存底层类型的引用

猜猜下面的程序会发生什么?

https://play.golang.org/p/pwhIwfHFzF9

如上所示,删除了 Perimeter 方法,程序不会编译且抛出一个编译错误。

program.go:22: cannot use Rect literal (type Rect) as type Shape in assignment:
        Rect does not implement Shape (missing Perimeter method)

上面的错误很明显,想要成功的实现一个接口,必须实现接口声明的全部方法。

☛ 空接口

当一个接口不含任何方法时,称之为空接口(empty interface),用interface{}表示。由于空接口没有方法,所以任何类型都实现了这个接口。

你是否感到疑惑, fmt 包的 Println函数是如何接收不同类型的数据并在控制台打印的。这么做完全可行,是因为空接口。接下来看如何实现。

新建一个函数 explain,入参为空接口,在函数内部打印它的 动态类型和值 。

https://play.golang.org/p/NhvO6Qjw_zp

在上面的程序中,创建了自定义 string 类型 MyString和结构体类型 Rect。由于explain函数接收空接口,所以能传入类型为 MyStringRect的变量。之所以空接口 i可以保存任何类型的值,是因为所有类型都实现了它。

☛ 实现多个接口

一个类型可以实现多个接口,看一个例子:

https://play.golang.org/p/YgW3NBxp8Fh

上面的程序中,创建了接口Shape含有方法Area、接口 Object含有方法 Volume 。结构体类型 Cube实现了这两个方法,即实现了这两个接口。因此,可以将结构体类型为 Cube的值赋给类型为 Shape 和 Object的变量。

我们期望 so都具有动态值 c。在类型为Shape的 s 上调用了Area方法,因为它定义了Area方法;同理,在类型为 Object的 o上调用了 Volume 方法。但是,如果在s上调用Volumeo上调用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静态类型Shapeo 的是 Object。为使程序正常运行,需要以某种方式提取这些接口的基础值(underlying value)。使用类型断言(type assertion)可以实现这一点。

☛ 类型断言

我们可以使用语法 i.(Type) 找到接口的基础动态值, 其中  i 是接口, Type 是实现接口 i 的类型 。Go 将检查**i**的动态类型是否与**Type**相同

因此,让我们重写前面的例子并提取接口的动态值。

https://play.golang.org/p/0e1XTpjuXJ_e

从上面的程序中,我们现在可以在变量 c 中访问接口 s 的基础值,它是 Cube类型的结构体。 现在 我们可以在 c 上使用  Area 和 Volume 方法。

在类型断言语法 i.(Type) 中, 如果 Type 没有实现接口 i(的类型),那么Go编译器会抛出错误。 如果 Type 实现了接口,但是 i 没有具体的 Type 值, 那么Go 会在运行时出现混乱。幸运的是还有另一种类型断言语法的变体,即

value, ok := i.(Type)

在上面的语法中, 如果 Type实现接口 i (的类型)  并且 i 有具体类型Type,我们可以使用 ok 变量进行检查。如果确实如此,那么 ok 将为 true, 否则为 false,其值为 **的值为零.

我还有个问题,我们如何知道接口的基础值是否实现了其他接口 ?对于类型断言也可以这样做,如果类型断言语法中的 Typeinterface ,然后 go 会检查动态类型 i 是否实现 Type 接口

https://play.golang.org/p/Iu84WAzDEwx

因为 Cube 结构体不实现 Skin 接口,所以我们将 false 作为 ok2 的值和将 nil 作为 value2 的值。如果我们使用更简单的 v := i.(type) 语法,那么我们的程序就会因错误而崩溃。

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

☛ 类型转换

我们已经看到了 empty interface 的使用,再让我们来看一下之前所说的使用 explain 函数的例子,由于 explain 函数的参数类型是空接口,那我们就可以将任何参数传递给它。如果传递的参数是字符串,那么我们希望 explain 函数将字符串以大写形式打印出来。我们可以使用 strings 包中的 ToUpper 函数,但由于它只接受字符串作为参数,因此我们需要确保 explain 函数内部空接口 i 具体类型为 string

这可以使用 类型转换 来完成,类型转换的语法类似于类型断言,i.(type) 其中的 i 是接口,type 是固定关键字。使用这个,我们可以得到接口的具体类型而不是值,但是这种语法只能在 switch 语句中

让我们看一个例子

https://play.golang.org/p/ItSSq3VDMbB

在上面程序中使用了 type switch 来修改了 explain 函数,当任何类型调用 explain 函数时, i 接收其值,并将其类型作为动态类型。在 switch 中使用 i.(type) 语句,我们将访问该动态类型,在 switch 中使用 cases ,我们可以执行条件操作。对于 string 的转换大小写,我们使用了 strings.ToUpper 函数将字符串转换为大小写。但是因为它只接受 string 类型,所以我们需要 i 的基础值为字符串类型,因此我们使用了 type assertion

☛ 嵌入接口

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

https://play.golang.org/p/s2U79IDaKqF

在上面的程序中,Cube 实现了 AreaVolume 方法,因此它就实现了 ShapeObject 接口。但由于 Material 接口是这些接口的嵌入式接口,那么 Cube 也必须实现它。这是因为与 anonymous nested struct 类似,嵌套接口的所有方法都上升为父接口。

译者注:anonymous nested struct 译为:匿名嵌套结构

☛  pointer 与 value 接收器

到目前为止,我们已经看到了值接收器的方法,那么让我们看看,接口是否可以接受指针接收器的方法。

https://play.golang.org/p/vEkRuYo1JKu

上面的程序不会通过编译,go 会抛出编译错误。

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

到底咋回事呢?我们可以清楚地看到结构类型 Rect 已经实现了 Shape 接口的所有方法,那为什么会出现 Rect does not implement Shape 错误呢?如果你仔细看错误信息,它已经告诉你 Area method has pointer receiver所以 Area 方法有一个指针接收器

好吧,我们在 structs 课程中看到,一个带有指针接收器的方法可以同时处理指针或值,如果我们在上面的程序中使用 r.Area() ,那么它就会很好的通过编译。

但对于接口,如果一个方法有指针接收器,那么接口将有动态类型的指针,而不是动态的值。因此,我们将一个类型值赋给一个接口变量时,我们需要分配类型值的指针,让我们用这个概念重写上面的程序。

https://play.golang.org/p/3OY4dBOSXdL

我们只需在第 25 行中修改,这里我们用指向 r 的指针,而不是 r 的值。因此,s 的具体值是一个指针,最后,以上程序会正常编译。

☛ 接口的使用

我们学习了接口,可以看到它们可以采取不同的形式。这就是 polymorphism 的定义,在需要传递许多类型参数的 functionsmethods 时,接口非常有用,比如 Println 函数,它接受所有类型的值。

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

这也是一个变量函数。

当多个类型实现相同的接口时,使用相同的代码很容易实现它们。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/rungo/interfaces-in-g...

译文地址:https://learnku.com/go/t/30182

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!