接口类型
Go 应用骨架(Application Skeleton):指将应用代码中的业务逻辑、算法实现逻辑、错误处理逻辑等“皮肉”逐一揭去后所呈现出的应用结构。
这就好比下面这个可爱的 Gopher(地鼠)通过 X 光机所看到的骨骼结构:
从静态角度去看,能清晰地看到应用程序的组成部分以及各个部分之间的连接,将其理解为 Go 应用内部的耦合设计;
从动态角度去看,能看到这幅骨架上可独立运动的几大机构,可以理解为应用的并发设计。
一个良好的骨架设计决定了应用的健壮性、灵活性与扩展性,甚至是应用的运行效率。
# 1. 接口类型
接口类型是==由 type 和 interface 关键字定义的一组方法集合==,其中,方法集合唯一确定了这个接口类型所表示的接口。
# 1.1 典型的接口类型
如下 MyInterface 的定义:
type MyInterface interface {
M1(int) error
M2(io.Writer, ...string)
}
2
3
4
接口类型 MyInterface 所表示的接口的方法集合,包含两个方法 M1 和 M2。之所以称 M1 和 M2 为“方法”,更多是从这个接口的实现者的角度考虑的。但从上面接口类型声明中各个“方法”的形式上来看,这更像是不带有 func 关键字的函数名 + 函数签名(参数列表 + 返回值列表)的组合。
方法的参数列表中形参名字与返回值列表中的具名返回值,都不作为区分两个方法的凭据。
比如下面的 MyInterface 接口类型的定义与上面的 MyInterface 接口类型定义都是等价的:
type MyInterface interface {
M1(a int) error
M2(w io.Writer, strs ...string)
}
type MyInterface interface {
M1(n int) error
M2(w io.Writer, args ...string)
}
2
3
4
5
6
7
8
9
Go 语言要求接口类型声明中的方法必须是具名的,并且方法名字在这个接口类型的方法集合中是唯一的。Go 1.14 版本以后,Go 接口类型允许嵌入的不同接口类型的方法集合存在交集,但前提是交集中的方法不仅名字要一样,它的函数签名部分也要保持一致,也就是参数列表与返回值列表也要相同,否则 Go 编译器照样会报错。
type Interface1 interface {
M1()
}
type Interface2 interface {
M1(string)
M2()
}
type Interface3 interface{
Interface1
Interface2 // 编译器报错:duplicate method M1
M3()
}
2
3
4
5
6
7
8
9
10
11
12
13
Interface3 嵌入了 Interface1 和 Interface2,但后两者交集中的 M1 方法的函数签名不同,导致了编译出错:
# 1.2 首字母小写的接口类型
在 Go 接口类型的方法集合中放入首字母小写的非导出方法也是合法的。
在 Go 标准库中也有非导出方法的接口类型定义,比如 context 包中的 canceler 接口类型,它的代码如下:
// $GOROOT/src/context.go
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
2
3
4
5
6
7
8
# 1.3 空接口类型
**空接口类型:**如果一个接口类型定义中没有一个方法,那么它的方法集合就为空。
比如下面的 EmptyInterface 接口类型:
type EmptyInterface interface {
}
2
3
这个方法集合为空的接口类型就被称为空接口类型,但通常不需要自己显式定义这类空接口类型,直接使用interface{}这个类型字面值作为所有空接口类型的代表就可以了。
# 1.4 接口类型变量
接口类型一旦被定义后,它就和其他 Go 类型一样可以用于声明变量,比如:
var err error // err是一个error接口类型的实例变量
var r io.Reader // r是一个io.Reader接口类型的实例变量
2
这些类型为接口类型的变量被称为接口类型变量,如果没有被显式赋予初值,接口类型变量的默认值为 nil。如果要为接口类型变量显式赋予初值,就要为接口类型变量选择合法的右值。
Go 规定:如果一个类型 T 的方法集合是某接口类型 I 的方法集合的等价集合或超集,我们就说类型 T 实现了接口类型 I,那么类型 T 的变量就可以作为合法的右值赋值给接口类型 I 的变量。
如果一个变量的类型是空接口类型,由于空接口类型的方法集合为空,这就意味着任何类型都实现了空接口的方法集合,所以可以将任何类型的值作为右值,赋值给空接口类型的变量。
比如下面例子:
var i interface{} = 15 // ok
i = "hello, golang" // ok
type T struct{}
var t T
i = t // ok
i = &t // ok
2
3
4
5
6
空接口类型的这一可接受任意类型变量值作为右值的特性,让他成为 Go 加入泛型语法之前唯一一种具有“泛型”能力的语法元素,包括 Go 标准库在内的一些通用数据结构与算法的实现,都使用了空类型interface{}作为数据元素的类。
# 1.5 类型断言
Go 语言支持接口类型变量赋值的“逆操作”,也就是通过接口类型变量“还原”它的右值的类型与值信息,这个过程被称为**“类型断言(Type Assertion)”**。
类型断言通常使用下面的语法形式:
v, ok := i.(T)
其中 i 是某一个接口类型变量,如果 T 是一个非接口类型且 T 是想要还原的类型,那么这句代码的含义就是断言存储在接口类型变量 i 中的值的类型为 T。
如果接口类型变量 i 之前被赋予的值确为 T 类型的值,那么这个语句执行后,左侧“comma, ok”语句中的变量 ok 的值将为 true,变量 v 的类型为 T,它值会是之前变量 i 的右值。如果 i 之前被赋予的值不是 T 类型的值,那么这个语句执行后,变量 ok 的值为 false,变量 v 的类型还是那个要还原的类型,但它的值是类型 T 的零值。
类型断言也支持下面这种语法形式:
v := i.(T)
但在这种形式下,一旦接口变量 i 之前被赋予的值不是 T 类型的值,那么这个语句将抛出 panic。如果变量 i 被赋予的值是 T 类型的值,那么变量 v 的类型为 T,它的值就会是之前变量 i 的右值。由于可能出现 panic,所以并不推荐使用这种类型断言的语法形式。
类型断言的语义:
var a int64 = 13
var i interface{} = a
v1, ok := i.(int64)
fmt.Printf("v1=%d, the type of v1 is %T, ok=%t\n", v1, v1, ok) // v1=13, the type of v1 is int64, ok=true
v2, ok := i.(string)
fmt.Printf("v2=%s, the type of v2 is %T, ok=%t\n", v2, v2, ok) // v2=, the type of v2 is string, ok=false
v3 := i.(int64)
fmt.Printf("v3=%d, the type of v3 is %T\n", v3, v3) // v3=13, the type of v3 is int64
v4 := i.([]int) // panic: interface conversion: interface {} is int64, not []int
fmt.Printf("the type of v4 is %T\n", v4)
2
3
4
5
6
7
8
9
10
如果v, ok := i.(T)中的 T 是一个接口类型,那么类型断言的语义就会变成:断言 i 的值实现了接口类型 T。
如果断言成功,变量 v 的类型为 i 的值的类型,而并非接口类型 T。
如果断言失败,v 的类型信息为接口类型 T,它的值为 nil
T 为接口类型的示例:
type MyInterface interface {
M1()
}
type T int
func (T) M1() {
println("T's M1")
}
func main() {
var t T
var i interface{} = t
v1, ok := i.(MyInterface)
if !ok {
panic("the value of i is not MyInterface")
}
v1.M1()
fmt.Printf("the type of v1 is %T\n", v1) // the type of v1 is main.T
i = int64(13)
v2, ok := i.(MyInterface)
fmt.Printf("the type of v2 is %T\n", v2) // the type of v2 is <nil>
// v2 = 13 // cannot use 1 (type int) as type MyInterface in assignment: int does not implement MyInterface (missing M1 method)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
通过the type of v2 is ,其实是看不出断言失败后的变量 v2 的类型的,但通过最后一行代码的编译器错误提示,我们能清晰地看到 v2 的类型信息为 MyInterface。