Go入门指南系列-XI-IX-空接口
11.9.1 概念
空接口或者最小接口 不包含任何方法,它对实现不做任何要求:
1 | type Any interface {} |
任何其他类型都实现了空接口(它不仅仅像 Java/C#
中 Object
引用类型),any
或 Any
是空接口一个很好的别名或缩写。
空接口类似 Java/C#
中所有类的基类: Object
类,二者的目标也很相近。
可以给一个空接口类型的变量 var val interface {}
赋任何类型的值。
示例 11.8 empty_interface.go:
1 | package main |
输出:
val has the value: 5
val has the value: ABC
val has the value: &{Rob Pike 55}
Type pointer to Person *main.Person
在上面的例子中,接口变量 val
被依次赋予一个 int
,string
和 Person
实例的值,然后使用 type-switch
来测试它的实际类型。每个 interface {}
变量在内存中占据两个字长:一个用来存储它包含的类型,另一个用来存储它包含的数据或者指向数据的指针。
示例 emptyint_switch.go 说明了空接口在 type-switch
中联合 lambda
函数的用法:
1 | package main |
输出:
any hello is a special String!
练习 11.9 simple_interface3.go:
继续练习 11.2,在它中添加一个 gI()
函数,它不再接受 Simpler
类型的参数,而是接受一个空接口参数。然后通过类型断言判断参数是否是 Simpler
类型。最后在 main
使用 gI()
取代 fI()
函数并调用它。确保你的代码足够安全。
11.9.2 构建通用类型或包含不同类型变量的数组
在 7.6.6 中我们看到了能被搜索和排序的 int
数组、float
数组以及 string
数组,那么对于其他类型的数组呢,是不是我们必须得自己编程实现它们?
现在我们知道该怎么做了,就是通过使用空接口。让我们给空接口定一个别名类型 Element
:type Element interface{}
然后定义一个容器类型的结构体 Vector
,它包含一个 Element
类型元素的切片:
1 | type Vector struct { |
Vector
里能放任何类型的变量,因为任何类型都实现了空接口,实际上 Vector
里放的每个元素可以是不同类型的变量。我们为它定义一个 At()
方法用于返回第 i
个元素:
1 | func (p *Vector) At(i int) Element { |
再定一个 Set()
方法用于设置第 i
个元素的值:
1 | func (p *Vector) Set(i int, e Element) { |
Vector
中存储的所有元素都是 Element
类型,要得到它们的原始类型(unboxing:拆箱)需要用到类型断言。TODO:The compiler rejects assertions guaranteed to fail,类型断言总是在运行时才执行,因此它会产生运行时错误。
练习 11.10 min_interface.go / minmain.go:
仿照 11.7 中开发的 Sorter
接口,创建一个 Miner
接口并实现一些必要的操作。函数 Min()
接受一个 Miner
类型变量的集合,然后计算并返回集合中最小的元素。
11.9.3 复制数据切片至空接口切片
假设你有一个 myType
类型的数据切片,你想将切片中的数据复制到一个空接口切片中,类似:
1 | var dataSlice []myType = FuncReturnSlice() |
可惜不能这么做,编译时会出错:cannot use dataSlice (type []myType) as type []interface { } in assignment
。
原因是它们俩在内存中的布局是不一样的(参考 Go wiki)。
必须使用 for-range
语句来一个一个显式地赋值:
1 | var dataSlice []myType = FuncReturnSlice() |
11.9.4 通用类型的节点数据结构
在 10.1 中我们遇到了诸如列表和树这样的数据结构,在它们的定义中使用了一种叫节点的递归结构体类型,节点包含一个某种类型的数据字段。现在可以使用空接口作为数据字段的类型,这样我们就能写出通用的代码。下面是实现一个二叉树的部分代码:通用定义、用于创建空节点的 NewNode
方法,及设置数据的 SetData
方法。
示例 11.10 node_structures.go:
1 | package main |
11.9.5 接口到接口
一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法。这个转换是在运行时进行检查的,转换失败会导致一个运行时错误:这是 Go
语言动态的一面,可以拿它和 Ruby
和 Python
这些动态语言相比较。
假定:
1 | var ai AbsInterface // declares method Abs() |
那么下面的语句和类型断言是合法的:
1 | empty = pp // everything satisfies empty |
下面是函数调用的一个例子:
1 | type myPrintInterface interface { |
x
转换为 myPrintInterface
类型是完全动态的:只要 x
的底层类型(动态类型)定义了 print
方法这个调用就可以正常运行(译注:若 x
的底层类型未定义 print
方法,此处类型断言会导致 panic
,最佳实践应该为 if mpi, ok := x.(myPrintInterface); ok { mpi.print() }
,参考 11.3 章节)。