[奔跑的 Go] 教程十四、 Go 语言接口(Interface)
接口的定义是非常棒的,而且也是实现 Go 中多态性的唯一途径。
☛ 什么是接口?#
我们在结构和方法课程上讨论了许多关于 对象 和 行为 的内容。我们看到了结构(以及其他非结构类型)实现方法。interface
是 方法签名 的集合,可以由 对象 来实现。因此,接口定义了对象的行为。
例如,Dog
会 walk
和 bark
。如果一个接口声明了 walk
和 bark
的方法签名,而 Dog
实现了 walk
和 bark
方法,那么 Dog
将实现该接口。
接口的主要工作是只提供方法名、输入参数和返回类型组成的方法签名。方法的声明和实现取决于类型(如结构体类型)。
如果原来你是面向对象(OOP)编程,那么你可能在实现接口时经常使用 implement
关键字。但是在 Go 中,你并没有显式的提到类型是否实现接口。如果类型实现接口中定义的签名方法,则该类型就实现了该接口。比如说,如果它像鸭子一样走路,像鸭子一样游泳和像鸭子一样叫,那么它就是鸭子。
☛ 声明接口#
与 struct
类似,我们需要使用类型别名和关键字 interface
简化接口的声明。
type Shape interface {
Area() float64
Perimeter() float64
}
在上面,我们定义了 Shape
接口,它有两个方法 Area
和 Perimeter
,这两个方法不接受参数并返回 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
语法是指接口的动态类型。但是实际上,接口的类型 s
是 Shape
。
☛ 实现接口#
根据接口 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
方法。此外,结构体类型为 Rect
的 s
和'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
函数接收空接口,所以能传入类型为 MyString
和 Rect
的变量。之所以空接口 i
可以保存任何类型的值,是因为所有类型都实现了它。
☛ 实现多个接口#
一个类型可以实现多个接口,看一个例子:
https://play.golang.org/p/YgW3NBxp8Fh
上面的程序中,创建了接口 Shape
含有方法 Area
、接口 Object
含有方法 Volume
。结构体类型 Cube
实现了这两个方法,即实现了这两个接口。因此,可以将结构体类型为 Cube
的值赋给类型为 Shape
和 Object
的变量。
我们期望 s
和 o
都具有动态值 c
。在类型为 Shape
的 s
上调用了 Area
方法,因为它定义了 Area
方法;同理,在类型为 Object
的 o
上调用了 Volume
方法。但是,如果在 s
上调用 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
。为使程序正常运行,需要以某种方式提取这些接口的基础值(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
,其值为 ** 的值为零.
我还有个问题,我们如何知道接口的基础值是否实现了其他接口 ?对于类型断言也可以这样做,如果类型断言语法中的 Type
是 interface
,然后 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
实现了 Area
和 Volume
方法,因此它就实现了 Shape
和 Object
接口。但由于 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
的定义,在需要传递许多类型参数的 functions
和 methods
时,接口非常有用,比如 Println
函数,它接受所有类型的值。
func Println(a ...interface{}) (n int, err error)
这也是一个变量函数。
当多个类型实现相同的接口时,使用相同的代码很容易实现它们。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: