第七章 接口
接口的本质是 "抽象"。Go 语言中,接口是隐式实现的,不需要诸如 implements,extends 的关键字主动定义一个结构体类型打算实现哪些接口。这样设计的好处是:只要某个库的类型是可导出的,我们可以就地为它拓展接口,而不用改动原有的声明 ( 满足 OCP 原则 ) 。
7.1 接口即约定
如果说之前介绍的类型全部都是具体类型,那么接口就是一个抽象类型。接口的具体类型是不确定的,我们仅仅知道接口定义了哪些方法,这些方法各自接收哪些参数,又返回哪些内容。
这里举一个简单的例子:Phone 接口定义了一部手机所应当具备的基本功能:通话,短信,还有网上冲浪。
type Phone interface {
// 接口内部可以有方法,可以再内嵌其它接口,但是不能声明普通的成员。
// 这里定义的都是抽象方法,不需要使用 func 关键字定义,且没有 {} 语句块。
call()
text()
wifi()
}
那么,任何一个品牌的手机都应该具备这三个基本功能 ( 各个厂商如何实现的,我们也不关心 ) :
// 小米手机--------------------------
type XiaoMi struct {}
func (x XiaoMi) call() {
fmt.Print("Are you OK?")
}
func (x XiaoMi) text() {
fmt.Println("I'm fine, thank you!")
}
func (x XiaoMi) wifi() {
fmt.Println("to Indian Mi Bands!")
}
func (x XiaoMi) MiUI {
fmt.Println("Mi Ui")
}
// 锤子手机-----------------------------
type Smartisan struct {}
func (s Smartisan) call() {
fmt.Println("Lao Luo don't do this")
}
func (s Smartisan) text() {
fmt.Println("TNT yyds")
}
func (s Smartisan) wifi() {
panic("Li Jie Wan Sui")
}
顺带一提,GoLand 会随时检测我们新创建的结构体 ( 类型 ),一旦它实现了某个接口的所有功能,其左侧会亮起熟悉的绿色 I 字母图标。另外,我们还可以在结构体内按下 Ctrl + i 并通过搜索接口的方式直接创建出方法模板。
对于用户而言,它仅使用统一的 Phone 接口实现预期的功能,而不再关注具体的实现类型。
var aPhone Phone
// aPhone = Smartisan{}
// aPhone = ... 可以是任何实现了 Phone 接口约定的结构体类型,
// 接口带来了编程的可取代性。
aPhone = XiaoMi{}
aPhone.call()
aPhone.text()
// 不能使用
aPhone.MiUI()
在这个例子中,我们可以这样表述:XiaoMi 是 ( is-a 的关系 ) 一个 Phone 类型。当 XiaoMi 被当作一个 Phone 类型使用时,它独有的 MiUI 方法会被禁止直接调用 ( 当然,通过类型断言可以将其重新特化成 XiaoMi,这样我们就可以调用这个方法了,见后文 )。这非常容易理解,因为该方法并不在 Phone 接口的约束范畴。
如果希望某个类型的方法调用是传地址 ( 而非传值 ) 的,那么根据经验,我们会将方法的接收者设置为类型指针,此时称 *XiaoMi 是一个 Phone 类型。
type XiaoMi struct {}
func (x *XiaoMi) call() {
panic("implement me")
}
func (x *XiaoMi) text() {
panic("implement me")
}
func (x *XiaoMi) wifi() {
panic("implement me")
}
//....
aPhone = &XiaoMi{}
注意," XiaoMi 是一个 Phone " 和 "*XiaoMi 是一个 Phone 类型 " 是存在一些微妙区别的。假定 XiaoMi 现在的实现是这个样子:
type Phone interface {
// 接口内部可以有方法,可以再内嵌其它接口,但是不能声明普通的成员。
// 这里定义的都是抽象方法,不需要使用 func 关键字定义,且没有 {} 语句块。
call()
text()
wifi()
}
type XiaoMi struct {}
func (x *XiaoMi) call() {}
func (x XiaoMi) text() {}
func (x *XiaoMi) wifi() {}
此时, "XiaoMi 是一个 Phone 类型" 不成立,但是 " *XiaoMi 是一个 Phone 类型" 成立。
var p Phone = &XiaoMi{}
// 接收者为指针时,语法不报错,方法都可以调用。
p.text()
p.call()
p.wifi()
var q Phone = XiaoMi{}
// 接收者为指针时,语法报错,原因在于 XiaoMi 被认为仅仅实现了 text() 方法。
q.wifi()
q.call()
q.text()
以 XiaoMi 类型的角度去思考,它只实现了 text() 方法,因为该方法的接收者是 *XiaoMi 而不是 XiaoMi。这么看来,*XiaoMi 似乎也没实现 call() 方法,那为什么它却可以当作 Phone 去使用呢?
原因是 Go 提供了这样的语法糖:假定 p 是 *XiaoMi 类型,那么该方法调用: p.text() 会被自动转化为 (*p).text()。因此,可以认为 *XiaoMi 也是 text() 方法的接收者,但是反之,接收者 XiaoMi 却 "不享受这样的待遇"。
推广并总结一下:由于接收者为 T 的方法可以被 *T 直接调用,因此 *T 可能会实现比 T 更多的接口。但实际上,当实现某个接口时,方法的接收者一般都是统一的:要么就都是命名类型,要么就都是类型指针,总体而言,接收者为类型指针时更 "赚" 一些,因为它还避免了值复制的开销。
接口内部不能内嵌普通的类型成员,但是可以组合其它的接口。比如:
// Go 源码库乐于以 "单一职责原则" 定义一些只包含一个方法的接口,然后供其它更复杂的接口加以组合。
type Reader interface {
Read()
}
type Writer interface {
Write(string)
}
type IOChannel interface {
// 它相当于获得了 Reader 和 Writer 定义的抽象方法。
Reader
Writer
// 这不影响该接口定义自己的抽象方法。
Close()
}
以上述代码块为例,任何一个实现 IOChannel 接口的类型,它除了实现该结构自身定义的 Close() 方法之外,还要实现 Reader 和 Writer 接口定义的所有方法。
type FileChannel struct {}
func (f *FileChannel) Read() {
//panic("implement me")
}
func (f *FileChannel) Write(s string) {
//panic("implement me")
}
func (f *FileChannel) Close() {
//panic("implement me")
}
显然,任何实现 IOChannel 接口的类型 ( 比如 FileChannel ) 都可以去充当 Reader 或者是 Writer 接口,但是反之不成立。
7.2 空接口
故名思意,空接口就是 interface{},它里面什么都没有。从语义上来讲,它接近 Java 中的 Object 类型或者是 Scala 中的 Any ,因为任何类型即便是没有声明方法也可以认为是实现了 interface{} 接口。
如果留意之前曾使用的 fmt.Fprintf(...) 等方法,能够发现,其后续的 arg 参数正是 ...interface{} 类型,它使得用户在调用该函数时能够传入任何数据结构。
var any interface{}
any = 1
any = 1.00
any = false
但反之,其逆操作 ( 从一个 interface{} 中还原出具体的值 ) 需要借助类型断言来实现。
7.3 接口值与可比较性
一个接口类型的值包含了两个信息:该接口实际的具体类型 ( 动态类型 ) 以及该类型的值 ( 动态值 )。由于 Go 是一门编译性的语言,因此类型不能直接被当作是一个普通的值来看待,其动态类型是通过引入 类型描述符 来获得的。
下面代码块中的接口值 p 经历了一次声明和三次赋值:代码的注释部分标记了在每个过程中该接口值的动态类型以及动态值的变化。在一个接口刚被声明时,它的动态类型和动态值均指向 nil,此时它也被称作是一个 nil 接口。
注意,仅动态值为 nil 但动态类型非 nil 的接口和两者均为 nil 的接口是不一样的。
/*
p 此时的动态类型: nil
p 此时的值: nil
此时调用任何方法都会报错
*/
var p Phone
/*
p 此时的动态类型: *XiaoMi
p 此时的值: &XiaoMi{}
p.call() =>(&XiaoMi{}).call()
*/
p = &XiaoMi{}
p.call()
/*
p 此时的动态类型: *HuaWi
p 此时的值: &HuaWei{}
p.text() =>(&HuaWei{}).text()
*/
p = &HuaWei{}
p.text()
/*
p 此时的动态类型: nil
p 此时的值: nil
此时调用任何方法都会报错
*/
p = nil
两个接口值是可以比较的。如果两个接口值的动态类型相同,且动态值在可比较的前提下也完全相等,那么这两个接口值就相同,进而,接口可以充当 map 散列表的 Key,或者是 switch 语句的操作数。
但是,如果两个接口的动态类型相同,但是其动态值是不可比较的类型,那么这个比较就会以宕机告终。比如,我们将切片 []int8 重新命名类型,然后就地拓展一个 Iterable 接口 :
type IntSeq []int8
type Iterable interface {
foreach()
}
func (s IntSeq) foreach() {
for i,v := range *s {
fmt.Printf("[%v](%v)",i,v)
}
}
下述代码块企图对两个 Iterable ( 或者说对两个 []int8) 类型的值进行比较,显然,程序会出错:
var seq1 Iterable
var seq2 Iterable
// IntSeq 是[]int8 的命名别称,因此 IntSeq{1,3,4} 等价于 []int8{1,3,4}
seq1 = IntSeq{1,3,4}
seq2 = IntSeq{1,3,4}
/*
panic: runtime error: comparing uncomparable type main.IntSeq
*/
fmt.Print(seq1 == seq2)
假设 Iterable 接口的实现者不是 IntSeq 而是 *IntSeq,那么这段程序就可以正常运行。原因是:虽然切片值是不可比较的,但是切片的地址是可以比较的。此时比较两个接口动态值是否相同,实际上是在检查两个切片的地址是否指向了同一个内存区域。
var seq1 Iterable
var seq2 Iterable
// IntSeq 是[]int8 的命名别称,因此 IntSeq{1,3,4} 等价于 []int8{1,3,4}
seq1 = &IntSeq{1,3,4}
// 如果 seq2 = seq1 ,那么程序的运行结果将是 true.
seq2 = &IntSeq{1,3,4}
// 程序的运行结果是 false.
fmt.Print(seq1 == seq2)
另外,通过 fmt.Printf 和 %T 可以令 fmt 通过反射的形式获取到某个接口的动态类型。
var seq1 Iterable
// IntSeq 是[]int8 的命名别称,因此 IntSeq{1,3,4} 等价于 []int8{1,3,4}
seq1 = &IntSeq{1,3,4}
// *main.IntSeq
fmt.Printf("%T",seq1)
7.4 sort.Interface 接口
接下来我们要了解 Go 标准库当中提供的两个重要接口,在这里首先介绍 sort.Interface 。排序是在编程工作中的通用操作,因此它显然不适合与任意一个具体的元素类型进行绑定。
在其它语言中,排序方法通常都是和已有的序列类型绑定,比如 Java 中的 List ,然后元素类型再通过约束类型边界的形式令其满足 "可排序" 的特征。但在 Go 语言中,sort.Sort(...) 方法允许我们对任何类型的序列进行排序,这个序列类型可以是切片,甚至是由我们自行定义的,满足 "索引序列" 特征的结构体。并且,Go 丝毫不关心序列的元素是什么类型。
但是在此之前,我们需要提前令该序列实现 sort.Interface 接口,以告知程序如何获取这个序列的长度 Len(),如何判别元素 A 小于另一个元素 B less(..),如何交换两个元素。
下面用两个示例介绍如何配合 sort.Interface 和 sort.Sort(..) 完成对序列的排序。这里规定元素类型是 Person,在第一个例子中,序列选择原生的切片类型:
type Persons []Person
type Person struct {age int8}
func (p *Persons) Len() int {
// 由于 Persons 本质上是 []Person 类型,因此可以仅通过调用 len() 方法快速得到长度。
return len(*p)
}
func (p *Persons) Less(i, j int) bool {
// 这里声明 pp 仅仅是为了避免下方代码使用 (*p)[x] 的繁琐写法,下同
pp := *p
if pp[i].age < pp[j].age {
return true
}
return false
}
func (p *Persons) Swap(i, j int) {
pp := *p
// 多重赋值的写法,在之前提到过。
pp[i],pp[j] = pp[j],pp[i]
}
而在第二个例子中,我们选择对自定义的 PersonList 序列 ( 它本质上是通过递归组织起来的链表) 进行排序。由于 sort.Interface 基于 "索引" 号进行比较,因此这里要额外完善 "索引" 查找的 Get(...) 方法。
type PersonList struct {
head *node
}
type node struct {
Person
next *node
}
/*
注意, index 值从 0 开始,在以下两种情况下程序会宕机:
index < 0 或者 index 超过了列表长度;
列表的头结点为 nil。
*/
func (ps *PersonList) Get(index int) (target *node) {
// 定义内部递归的局部函数
var _get func(*node,int) *node
_get = func (n *node,step int) (target *node){
switch {
case step == 0: target = n
default:
target = _get(n.next,step-1)
}
return
}
switch {
case ps.head == nil:
panic("该 PersonList 为 nil。")
case index < 0:
panic("越界下标")
case index > ps.Len()-1:
//如果该列表的长度为 0,那么程序必定在此处宕机。
panic("越界下标")
}
return _get(ps.head,index)
}
/*
通过递归的方式来获取列表长度。
如果列表的头结点为 nil,则长度为 0,
*/
func (ps *PersonList) Len() (len int) {
var iterator func(*node,int) int
iterator = func (n *node, acc int) (len int) {
switch {
case n.next == nil: len = acc
default:
// 注意, 如果直接写 iterator(....) ,函数不会继续递归,而是直接 return.
len = iterator(n.next,acc+1)
}
return
}
if ps.head == nil {
return 0
}
len = iterator(ps.head,1)
return
}
func (ps *PersonList) Less(i, j int) bool {
return ps.Get(i).age < ps.Get(j).age
}
func (ps *PersonList) Swap(i, j int) {
ps.Get(i).Person,ps.Get(j).Person = ps.Get(j).Person,ps.Get(i).Person
}
7.5 error 接口
其实我们早在之前调用 os 等资源库的例子中就接触过这个 error 接口。
// 这个 err 实现了 error 接口
open, err := os.Open("/src/name/touch.txt")
error 接口的约束非常简单直接:通过 Error() ( 该方法返回一个 string 类型 ) 对外暴露错误信息。
现在,我们可以通过调用 errors.New 函数主动创建一个 error :
func div(a,b float64) (r float64,err error){
err = nil
if b == 0 {
// New 函数相当于是 error 的 "构造器"
err = errors.New("除数不可为0")
r = math.NaN()
}
r = a / b
return
}
更常用的做法是通过 fmt.Errorf 来实现,因为我们可以通过格式化的方式构造字符串。
err = fmt.Errorf("非法的除数:%v",b)
7.6 类型断言
类型断言相当于是将具体类型泛化为接口的逆操作:尝试将某个接口 I 转化成某个具体的类型或者是更加特化的接口 O ( 并不是强制性的,但通常我们没有必要断言更泛化的接口),如果成功了,那么就可以获得更多的拓展方法。
var aPhone Phone
aPhone = &XiaoMi{}
// 由于 MiUI 是小米手机独有的方法,因此这里要借助类型断言将其强制转换为小米手机。
// 语法是 (接口值).(目标类型/接口)
mi := aPhone.(*XiaoMi)
mi.MiUI()
类型断言并不总是成功的,因此它有可能引发系统宕机。
var aPhone Phone
aPhone = &HuaWei{}
// 编译器不报错,但是运行报错
// panic: interface conversion: main.Phone is *main.HuaWei, not *main.XiaoMi
mi := aPhone.(*XiaoMi)
mi.MiUI()
7.7 类型分支
接口有完全不同的两种使用风格。截至目前,我们利用接口的目的是:抽象方法,提供约束,屏蔽实现,分离代码。这种使用风格称之为子类型多态 ( subtype polymorphism ) :通俗的话就是 "基类" 能做的活 "派生类" 都能做。在 C++ / Java 的世界观中,这种风格通过 "继承" 来体现。
另一种使用风格是,利用接口可以容纳多种类型的能力,将这些多个类型看做是一个联合 ( union ) 。函数接收联合类型,并通过判断它的实际类型来采取 ( 可能是截然不同 ) 的处理策略。当然,和完全的 泛型 相比,联合类型是有限数量的。这种接口的使用风格称之为特设多态 ( ad hoc polymorphism ) 。
下面用一个简单的例子来演示:函数通过 interface{} 以接收任何类型的参数,然后根据参数的类型在控制台打印不同的内容:
func is(a interface{}){
// a.(type) type 是关键字,该写法在 switch 语句中用于提取 a 的实际类型。
switch a.(type) {
case nil: fmt.Print("空指针")
case int: fmt.Print("int 类型")
case float32: fmt.Print("float32 类型")
case float64: fmt.Print("float64 类型")
default:
fmt.Print("其它的类型...")
}
}