The Way to Go(五)

接口

接口是什么

Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。

但是 Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来 说明 对象的行为:如果谁能搞定这件事,它就可以用在这儿。

接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。

通过如下格式定义接口:

1
2
3
4
5
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}

上面的 Namer 是一个 接口类型。

按照约定,只包含一个方法的)接口的名字由方法名加 [e]r 后缀组成,例如 PrinterReaderWriterLoggerConverter 等等。还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头(像 .NET 或 Java 中那样)。

Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。

不像大多数面向对象编程语言,在 Go 语言中接口可以有值,一个接口类型的变量或一个 接口值 :var ai Namerai 是一个多字(multiword)数据结构,它的值是 nil。它本质上是一个指针,虽然不完全是一回事。指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。

类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口。

实现某个接口的类型(除了实现接口方法外)可以有其他的方法。

一个类型可以实现多个接口。

接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)。

即使接口在类型之后才定义,二者处于不同的包中,被单独编译:只要类型实现了接口中的方法,它就实现了此接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

type Shaper interface {
Area() float32
}

type Square struct {
side float32
}

func (sq *Square) Area() float32 {
return sq.side * sq.side
}

func main() {
sq1 := new(Square)
sq1.side = 5

var areaIntf Shaper
areaIntf = sq1
// shorter,without separate declaration:
// areaIntf := Shaper(sq1)
// or even:
// areaIntf := sq1
fmt.Printf("The square has area: %f\n", areaIntf.Area())
}

输出:

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
2
cannot use sq1 (type *Square) as type Shaper in assignment:
*Square does not implement Shaper (missing Area method)

如果 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
5
if v, ok := varI.(T); ok {  // checked type assertion
Process(v)
return
}
// varI is not of type T

如果转换合法,vvarI 转换到类型 T的值,ok会是 true;否则v是类型 T 的零值,okfalse,也没有运行时错误发生。

多数情况下,我们可能只是想在if中测试一下ok 的值,此时使用以下的方法会是最方便的:

1
2
3
if _, ok := varI.(T); ok {
// ...
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
"fmt"
"math"
)

type Square struct {
side float32
}

type Circle struct {
radius float32
}

type Shaper interface {
Area() float32
}

func main() {
var areaIntf Shaper
sq1 := new(Square)
sq1.side = 5

areaIntf = sq1
// Is Square the type of areaIntf?
if t, ok := areaIntf.(*Square); ok {
fmt.Printf("The type of areaIntf is: %T\n", t)
}
if u, ok := areaIntf.(*Circle); ok {
fmt.Printf("The type of areaIntf is: %T\n", u)
} else {
fmt.Println("areaIntf does not contain a variable of type Circle")
}
}

func (sq *Square) Area() float32 {
return sq.side * sq.side
}

func (ci *Circle) Area() float32 {
return ci.radius * ci.radius * math.Pi
}

输出:

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
10
switch 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
9
switch areaIntf.(type) {
case *Square:
// TODO
case *Circle:
// TODO
...
default:
// TODO
}

使用方法集与接口

作用于变量上的方法实际上是不区分变量到底是指针还是值的。当碰到接口类型值时,这会变得有点复杂,原因是接口变量中存储的具体值是不可寻址的,幸运的是,如果使用不当编译器会给出错误。考虑下面的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"fmt"
)

type List []int

func (l List) Len() int {
return len(l)
}

func (l *List) Append(val int) {
*l = append(*l, val)
}

type Appender interface {
Append(int)
}

func CountInto(a Appender, start, end int) {
for i := start; i <= end; i++ {
a.Append(i)
}
}

type Lener interface {
Len() int
}

func LongEnough(l Lener) bool {
return l.Len()*10 > 42
}

func main() {
// A bare value
var lst List
// compiler error:
// cannot use lst (type List) as type Appender in argument to CountInto:
// List does not implement Appender (Append method has pointer receiver)
// CountInto(lst, 1, 10)
if LongEnough(lst) { // VALID:Identical receiver type
fmt.Printf("- lst is long enough\n")
}

// A pointer value
plst := new(List)
CountInto(plst, 1, 10) //VALID:Identical receiver type
if LongEnough(plst) {
// VALID: a *List can be dereferenced for the receiver
fmt.Printf("- plst is long enough\n")
}
}

讨论

lst 上调用 CountInto 时会导致一个编译器错误,因为 CountInto 需要一个Appender,而它的方法Append 只定义在指针上。 在 lst上调用LongEnough是可以的因为 ‘Len’ 定义在值上。

plst 上调用CountInto是可以的,因为CountInto 需要一个 Appender,并且它的方法 Append 定义在指针上。 在 plst上调用 LongEnough 也是可以的,因为指针会被自动解引用。

总结

在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以从具体类型 P 直接可以辨识的:

  • 指针方法可以通过指针调用
  • 值方法可以通过值调用
  • 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
  • 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址

将一个值赋值给一个接口时,编译器会确保所有可能的接口方法都可以在此值上被调用,因此不正确的赋值在编译期就会失败。

反射

反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。反射可以在运行时检查类型和变量,例如它的大小、方法和 动态

变量的最基本信息就是类型和值:反射包的 Type用来表示一个 Go 类型,反射包的 Value 为 Go 值提供了反射接口。

两个简单的函数,reflect.TypeOfreflect.ValueOf,返回被检查对象的类型和值。例如,x 被定义为:var x float64 = 3.4,那么 reflect.TypeOf(x)返回 float64reflect.ValueOf(x) 返回 <float64 Value>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"reflect"
)

func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
v := reflect.ValueOf(x)
fmt.Println("value:", v)
fmt.Println("type:", v.Type())
fmt.Println("kind:", v.Kind())
fmt.Println("value:", v.Float())
fmt.Println(v.Interface())
fmt.Printf("value is %5.2e\n", v.Interface())
y := v.Interface().(float64)
fmt.Println(y)
}

输出:

1
2
3
4
5
6
7
8
type: 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)

------ 本文结束 ------
0%