《Go语言四十二章经》第十九章 接口
19.1 接口是什么
Go语言接口定义了一组方法集合,但是这些方法集合仅仅只是被定义,它们没有在接口中实现。接口(interface)类型是Go语言的一种数据类型。而因为所有的类型包括自定义类型都实现了空接口interface,所以空接口interface可以被当做任意类型的数值。
Go 语言中的所有类型包括自定义类型都实现了interface接口,这意味着所有的类型如string、 int、 int64甚至是自定义的结构体类型都拥有interface空接口,这一点interface和Java中的Object类比较相似。
接口类型的未初始化变 量的值为nil。
var i interface{} = 99 // i可以是任何类型
i = 44.09
i = "All" // i 可接受任意类型的赋值
接口是一组抽象方法的集合,它必须由其他非接口类型实现,不能自我实现。Go 语言通过它可以实现很多面向对象的特性。
通过如下格式定义接口:
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
上面的 Namer 是一个接口类型,按照惯例,单方法接口由方法名称加上-er后缀或类似修改来命名,以构造代理名词:Reader,Writer,Formatter,CloseNotifier等。还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头等。
Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。如标准库io包中定义了下面2个接口:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
在Go语言中,如果接口的所有方法在某个类型方法集中被实现,则认为该类型实现了这个接口。
类型不用显式声明实现了接口,只需要实现接口所有方法,这样的隐式实现解藕了实现接口的包和定义接口的包。
同一个接口可被多个类型可以实现,一个类型也可以实现多个接口。实现了某个接口的类型,还可以有其它的方法。有时我们甚至都不知道某个类型定义的方法集巧合地实现了某个接口。这种灵活性使我们不用像JAVA语言那样需要显式implement,一旦类型不需要实现某个接口,我们甚至可以不改动任何代码。
类型需要实现接口方法集中的所有方法,一定是接口方法集中所有方法。类型实现了这个接口,那么接口类型的变量也就可以存放该类型的值。
如下代码所示,结构体A和类型I都实现了接口B的方法f(),所有这两种类型也具有了接口B的一切特性,可以将该类型的 值存储在接口B类型的变量中:
package main
import (
"fmt"
)
type A struct {
Books int
}
type B interface {
f()
}
func (a A) f() {
fmt.Println("A.f() ", a.Books)
}
type I int
func (i I) f() {
fmt.Println("I.f() ", i)
}
func main() {
var a A = A{Books: 9}
a.f()
var b B = A{Books: 99} // 接口类型可接受结构体A的值,因为结构体A实现了接口
b.f()
var i I = 199 // I是int类型引申出来的新类型
i.f()
var b2 B = I(299) // 接口类型可接受新类型I的值,因为新类型I实现了接口
b2.f()
}
程序输出:
A.f() 9
A.f() 99
I.f() 199
I.f() 299
如果接口在类型之后才定义,或者二者处于不同的包中。但只要类型实现了接口中的所有方法,这个类型就实现了此接口。
因此Go语言中接口具有强大的灵活性。
注意:接口中的方法必须要全部实现,才能实现接口。
19.2 接口嵌入
一个接口可以包含一个或多个其他的接口,但是在接口内不能嵌入结构体,也不能嵌入接口自身,否则编译会出错。
下面这两种嵌入接口自身的方式都不能编译通过:
// 编译错误:invalid recursive type Bad
type Bad interface {
Bad
}
// 编译错误:invalid recursive type Bad2
type Bad1 interface {
Bad2
}
type Bad2 interface {
Bad1
}
比如下面的接口 File 包含了 ReadWrite 和 Lock 的所有方法,它还额外有一个 Close() 方法。接口的嵌入方式和结构体的嵌入方式语法上差不多,直接写接口名即可。
type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock()
}
type File interface {
ReadWrite
Lock
Close()
}
19.3 类型断言
前面我们可以把实现了某个接口的类型值保存在接口变量中,但反过来某个接口变量属于哪个类型呢?如何检测接口变量的类型呢?这就是类型断言(Type Assertion)的作用。
接口类型I的变量 varI 中可以包含任何实现了这个接口的类型的值,如果多个类型都实现了这个接口,所以有时我们需要用一种动态方式来检测它的真实类型,即在运行时确定变量的实际类型。
通常我们可以使用类型断言(value, ok := element.(T))来测试在某个时刻接口变量 varI 是否包含类型 T 的值:
value, ok := varI.(T) // 类型断言
varI 必须是一个接口变量,否则编 译器会报错:invalid type assertion: varI.(T) (non-interface type (type of I) on left) 。
类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言:
var varI I
varI = T("Tstring")
if v, ok := varI.(T); ok { // 类型断言
fmt.Println("varI类型断言结果为:", v) // varI已经转为T类型
varI.f()
}
如果 断言成功,v 是 varI 转换到类型 T 的值,ok 会是 true;否则 v 是类型 T 的零值,ok 是 false,也没有运行时错误发生。
接口类型向普通类型转换有两种方式:Comma-ok断言和Type-switch测试。
通过Type-switch做类型判断
接口变量的类型可以使用一种特殊形式的 switch 做类型断言:
// Type-switch做类型判断
var value interface{}
switch str := value.(type) {
case string:
fmt.Println("value类型断言结果为string:", str)
case Stringer:
fmt.Println("value类型断言结果为Stringer:", str)
default:
fmt.Println("value类型不在上述类型之中")
}
可以用 Type-switch 进行运行时类型分析,但是在 type-switch 时不允许有 fallthrough 。Type-switch让我们在处理未知类型的数据时,比如解析 json 等编码的数据,会非常方便。
测试一个值是否实现了某个接口(Comma-ok断言)
我们想测试它是否实现了 I 接口,可以这样做类型断言:
// Comma-ok断言
var varI I
varI = T("Tstring")
if v, ok := varI.(T); ok { // 类型断言
fmt.Println("varI类型断言结果为:", v) // varI已经转为T类型
varI.f()
}
接口描述了一系列的行为,规定可以做什么行为,“当一个东西,走起来像鸭子,叫起来也像鸭子,游泳也像鸭子,那么我们可以认为他就是一只鸭子”。类型实现不同的接口将拥有不同的行为方法集合,这就是多态的本质。
下面是上面几个代码片段的完整代码文件:
package main
import (
"fmt"
)
type I interface {
f()
}
type T string
func (t T) f() {
fmt.Println("T Method")
}
type Stringer interface {
String() string
}
func main() {
// 类型断言
var varI I
varI = T("Tstring")
if v, ok := varI.(T); ok { // 类型断言
fmt.Println("varI 类型断言结果为:", v) // varI已经转为T类型
varI.f()
}
// Type-switch做类型判断
var value interface{} // 默认为零值
switch str := value.(type) {
case string:
fmt.Println("value类型断言结果为string:", str)
case Stringer:
fmt.Println("value类型断言结果为Stringer:", str)
default:
fmt.Println("value类型不在上述类型之中")
}
// Comma-ok断言
value = "