接口
接口是什么
Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。
但是 Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来 说明 对象的行为:如果谁能搞定这件事,它就可以用在这儿。
接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。
通过如下格式定义接口:
1 | type Namer interface { |
上面的 Namer
是一个 接口类型。
按照约定,只包含一个方法的)接口的名字由方法名加 [e]r
后缀组成,例如 Printer
、Reader
、Writer
、Logger
、Converter
等等。还有一些不常用的方式(当后缀 er
不合适时),比如 Recoverable
,此时接口名以 able
结尾,或者以 I
开头(像 .NET 或 Java 中那样)。
Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。
不像大多数面向对象编程语言,在 Go 语言中接口可以有值,一个接口类型的变量或一个 接口值 :var ai Namer
,ai
是一个多字(multiword)数据结构,它的值是 nil
。它本质上是一个指针,虽然不完全是一回事。指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。
类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口。
实现某个接口的类型(除了实现接口方法外)可以有其他的方法。
一个类型可以实现多个接口。
接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)。
即使接口在类型之后才定义,二者处于不同的包中,被单独编译:只要类型实现了接口中的方法,它就实现了此接口。
1 | package main |
输出:1
The square has area: 25.000000
上面的程序定义了一个结构体 Square
和一个接口 Shaper
,接口有一个方法 Area()
。
在 main()
方法中创建了一个Square
的实例。在主程序外边定义了一个接收者类型是 Square
方法的 Area()
,用来计算正方形的面积:结构体 Square
实现了接口 Shaper
。
所以可以将一个 Square
类型的变量赋值给一个接口类型的变量:areaIntf = sq1
。
现在接口变量包含一个指向 Square
变量的引用,通过它可以调用 Square
上的方法 Area()。当然也可以直接在 Square
的实例上调用此方法,但是在接口实例上调用此方法更令人兴奋,它使此方法更具有一般性。接口变量里包含了接收者实例的值和指向对应方法表的指针。
这是 多态 的 Go 版本,多态是面向对象编程中一个广为人知的概念:根据当前的类型选择正确的方法,或者说:同一种类型在不同的实例上似乎表现出不同的行为。
如果 Square
没有实现Area()
方法,编译器将会给出清晰的错误信息:
1 | cannot use sq1 (type *Square) as type Shaper in assignment: |
如果 Shaper
有另外一个方法 Perimeter()
,但是Square
没有实现它,即使没有人在 Square
实例上调用这个方法,编译器也会给出上面同样的错误。
类型断言:如何检测和转换接口变量的类型
一个接口类型的变量 varI
中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。在执行过程中动态类型可能会有所不同,但是它总是可以分配给接口变量本身的类型。通常我们可以使用 类型断言 来测试在某个时刻 varI
是否包含类型 T
的值:
1 | v := varI.(T) // unchecked type assertion |
varI 必须是一个接口变量,否则编译器会报错:invalid type assertion: varI.(T) (non-interface type (type of varI) on left)
。
类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言:1
2
3
4
5if v, ok := varI.(T); ok { // checked type assertion
Process(v)
return
}
// varI is not of type T
如果转换合法,v
是 varI
转换到类型 T
的值,ok
会是 true
;否则v
是类型 T
的零值,ok
是 false
,也没有运行时错误发生。
多数情况下,我们可能只是想在if
中测试一下ok
的值,此时使用以下的方法会是最方便的:1
2
3if _, ok := varI.(T); ok {
// ...
}
1 | package main |
输出:1
2
3
The type of areaIntf is: *main.Square
areaIntf does not contain a variable of type Circle
程序行中定义了一个新类型 Circle
,它也实现了 Shaper
接口。 t, ok := areaIntf.(*Square); ok
测试areaIntf
里是否一个包含 ‘Square’ 类型的变量,结果是确定的;然后我们测试它是否包含一个 ‘Circle’ 类型的变量,结果是否定的。
备注
如果忽略 areaIntf.(*Square)
中的*
号,会导致编译错误:impossible type assertion: Square does not implement Shaper (Area method has pointer receiver)
。
接口变量的类型也可以使用一种特殊形式的 switch
来检测:type-switch
1
2
3
4
5
6
7
8
9
10switch t := areaIntf.(type) {
case *Square:
fmt.Printf("Type Square %T with value %v\n", t, t)
case *Circle:
fmt.Printf("Type Circle %T with value %v\n", t, t)
case nil:
fmt.Printf("nil value: nothing to check?\n")
default:
fmt.Printf("Unexpected type %T\n", t)
}
输出:1
Type Square *main.Square with value &{5}
变量 t
得到了areaIntf
的值和类型, 所有case
语句中列举的类型(nil
除外)都必须实现对应的接口(在上例中即 Shaper),如果被检测类型没有在 case
语句列举的类型中,就会执行 default
语句。
可以用 type-switch
进行运行时类型分析,但是在 type-switch
不允许有fallthrough
。
如果仅仅是测试变量的类型,不用它的值,那么就可以不需要赋值语句,比如:1
2
3
4
5
6
7
8
9switch areaIntf.(type) {
case *Square:
// TODO
case *Circle:
// TODO
...
default:
// TODO
}
使用方法集与接口
作用于变量上的方法实际上是不区分变量到底是指针还是值的。当碰到接口类型值时,这会变得有点复杂,原因是接口变量中存储的具体值是不可寻址的,幸运的是,如果使用不当编译器会给出错误。考虑下面的程序:
1 | package main |
讨论
在 lst
上调用 CountInto
时会导致一个编译器错误,因为 CountInto
需要一个Appender
,而它的方法Append
只定义在指针上。 在 lst
上调用LongEnough
是可以的因为 ‘Len’ 定义在值上。
在plst
上调用CountInto
是可以的,因为CountInto
需要一个 Appender
,并且它的方法 Append
定义在指针上。 在 plst
上调用 LongEnough
也是可以的,因为指针会被自动解引用。
总结
在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以从具体类型 P 直接可以辨识的:
- 指针方法可以通过指针调用
- 值方法可以通过值调用
- 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
- 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址
将一个值赋值给一个接口时,编译器会确保所有可能的接口方法都可以在此值上被调用,因此不正确的赋值在编译期就会失败。
反射
反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。反射可以在运行时检查类型和变量,例如它的大小、方法和 动态
变量的最基本信息就是类型和值:反射包的 Type
用来表示一个 Go 类型,反射包的 Value
为 Go 值提供了反射接口。
两个简单的函数,reflect.TypeOf
和 reflect.ValueOf
,返回被检查对象的类型和值。例如,x
被定义为:var x float64 = 3.4
,那么 reflect.TypeOf(x)
返回 float64
,reflect.ValueOf(x)
返回 <float64 Value>
1 | package main |
输出:1
2
3
4
5
6
7
8type: float64
value: 3.4
type: float64
kind: float64
value: 3.4
3.4
value is 3.40e+00
3.4
x
是一个 float64
类型的值,reflect.ValueOf(x).Float()
返回这个 float64
类型的实际值;同样的适用于 Int(), Bool(), Complex(), String()
反射结构
有些时候需要反射一个结构类型。NumField()
方法返回结构内的字段数量;通过一个 for
循环用索引取得每个字段的值 Field(i)
。
我们同样能够调用签名在结构上的方法,例如,使用索引 n
来调用:Method(n).Call(nil)
。