上一节中谈到了面向对象,并且定义了一堆方法,从对象的角度定义了其操作的方法。然而,你是谁,我并不关心,我只关心你能为我干什么,是所有不同对象接触的一个界面。在Go中,接口就是这样一种抽象类型,它定义了一个对象的行为,即定义了你应该做什么,但具体怎么做,接口并不管。如果说封装是保护了代码实现的内部,那么接口则是规范了交互约定,保护了外部代码,两者是相辅相成的。定义接口对于团队协作,保护外部代码,维持历史软件版本非常重要。具体来说,接口是一个或几个方法签名的集合,如果一个类型定义了接口中所有方法,就实现了该接口。Unix/Linux系统中流传着一句话叫 "Everything is a file",即一切对象都是文件(更为确切的应当是一切对象都是文件描述符)设备是文件自身是文件、设备是文件、网络是文件,进程都可以是一个文件,可以文件是一个抽象,一切支持读写操作接口,事实上操作一个普通文件和操作一个设备,其底层差异是非常巨大的,但面对对象与接口设计让他们都成为了文件。
接口的定义与声明
// 定义接口
type InterfaceNameA interface {
Method1()
Method2()
}
type InterfaceNameB interface{
Method3()
Method4()
}
type InterfaceNameC interface{
InterfaceNameA
InterfaceNameB
Method5()
}
// 声明
var IN InterfaceNameA
// 调用
IN = DataType
IN.MethodX()
接口的定义不像结构体,其对顺序没有要求,或者说不区分顺序。在接口中可以直接写入方法签名或使用另一个接口。要实现一个接口,就必须实现接口中的所有方法。一个数据类型可以实现多个接口类型。
接口的声明需要通过 var
关键字实现,无法使用类型推导。接口类型的零值是 nil
,如果声明接口却没有赋值或没有实现,调用就会出错。
下面看一个具体的例子。假设名为"生长"的接口,里面包含"开花"、"结果"两个方法,用于演示植物生长过程。
package main
import (
"fmt"
"time"
)
// GrowUp interface has methods bloom, fructify
type GrowUp interface {
bloom() string
fructify() string
}
type sumflower string
type apple string
func (app apple) bloom() string {
return fmt.Sprintf("Apple Tree %s Bloom at %s", string(app), time.Now().Format("2006-01-02 15:04:05"))
}
func (app apple) fructify() string {
return fmt.Sprintf("Apple Tree %s Fructified at %s", string(app), time.Now().Format("2006-01-02 15:04:05"))
}
func (sf sumflower) bloom() string {
return fmt.Sprintf("Sumflower %s Bloom at %s", string(sf), time.Now().Format("2006-01-02 15:04:05"))
}
func (sf sumflower) fructify() string {
return fmt.Sprintf("Sumflower %s Fructified at %s", string(sf), time.Now().Format("2006-01-02 15:04:05"))
}
func main() {
Littlesmile := sumflower("LittleSmile")
Sweet := apple("Sweet")
var gup GrowUp
gup = Littlesmile
fmt.Println("This is", Littlesmile)
fmt.Println(gup.bloom())
time.Sleep(2 * time.Second)
fmt.Println(gup.fructify())
gup = Sweet
fmt.Println("This is", Sweet)
fmt.Println(gup.bloom())
time.Sleep(3 * time.Second)
fmt.Println(gup.fructify())
}
/* ------------Result----------
This is LittleSmile
Sumflower LittleSmile Bloom at 2018-11-20 00:44:33
Sumflower LittleSmile Fructified at 2018-11-20 00:44:35
This is Sweet
Apple Tree Sweet Bloom at 2018-11-20 00:44:35
Apple Tree Sweet Fructified at 2018-11-20 00:44:38
*/
//跟其他语言不同,Go 实现一个接口不需要显式说明,只要实现了,就可以使用。
在这个例子中,声明了两种植物向日葵(sumflower)和苹果,按照接口要求,都定义了“开花”和“结果”的方法,与前一节面向对象编程时的main函数不同,我们没有单纯的使用创建对象,调用对象方法,而是创建了对象,声明了GroupUp接口类型变量 gup
,然后神奇的将 apple
对象 Sweet
与 sumflower
对象 Littlesmile
赋值给了 gup
,然后还成功的调用了他们的方法展现了生长的过程。这就是接口,它只关心方法,不关心对象,这对于进一步处理更多类型的“生长”留下了良好的基础,因为你不必再关心那些植物有哪些可用的方法了。如果有一个植物没有实现 GrowUp
接口,然后赋值给 gup
会发生什么,会在编译时生成panic导致失败。对于 var gup GrowUp
如果没有经过 gup =
的赋值,那么 gup
就会是一个 nil
,这也是接口唯一可以比较的对象 gup == nil
,如果判定成功,那么对于后续接口的使用就应该立即避免,否则会引发 panic
。
接口是一个interface
类型变量,那么它就和其他所有类型一致,所有类型值可以使用的习惯,在接口处也全部成立。例如,切片、循环、指针等等。
sumflowerA := sumflower("sumflowerA")
sumflowerB := sumflower("sumflowerB")
appleA := apple("appleA")
appleB := apple("appleB")
gups := []GrowUp{sumflowerA, sumflowerB, appleA, appleB}
for _, v := range gups {
fmt.Println(v.bloom())
}
/* ----result----------
Sumflower sumflowerA Bloom at 2018-11-20 10:03:34
Sumflower sumflowerB Bloom at 2018-11-20 10:03:34
Apple Tree appleA Bloom at 2018-11-20 10:03:34
Apple Tree appleB Bloom at 2018-11-20 10:03:34
如果一个接口没有约定方法 type i interface{}
,将其称之为空接口,所有类型都实现了空接口。空接口有什么用呢?如上那个例子,空接口可以作为任何类型的接收器,用于承载任何数据。之前都没有做任何接口数据的介绍,只关注方法,接口确实是这样,但不代表接口没有值。接口在Go内部可视为(type, value)
表达,type 为底层一种数据类型,value就是value,正是由于这种特性,接口才能承载任何对象。看下面例子,可能会理解的更为直观一点。
for _, v := range gups {
fmt.Printf("%T %+v",v,v)
}
/* ---result-----
main.sumflower sumflowerA
main.sumflower sumflowerB
main.apple appleA
main.apple appleB
*/
假如知道底层类型,那是不是可以获取是值呢,答案是肯定的,这需要用到类型断言,怎么叫断言,就是对数据强制转换。typevalue, ok := i.(type)
,例如延续上面的例子
// 将循环的语句修改为
fmt.Printf("%T %+v\n", v.(sumflower), v.(sumflower))
/* ----- result ---------
main.sumflower sumflowerA
main.sumflower sumflowerB
panic: interface conversion: main.GrowUp is main.apple, not main.sumflower
断言结果无非两种:如果是 sumflower 类型,那么就断言成功,获取sumflower的值,如果是 apple 类型,那么断言失败,导致运行panic异常。如果完整使用断言 typevalue, ok := i.(type)
这时断言失败,就不会引发 panic。除非知道自己干什么,否则不要轻易断言。
sfv, ok := v.(sumflower)
if ok {
fmt.Printf("%T %+v\n", sfv, sfv)
}
/* --------result------------
main.sumflower sumflowerA
main.sumflower sumflowerB
*/
还记得fmt.Println()
,它可以打印任何类型值,看一下 fmt.Println()
签名,func Println(a ...interface{}) (n int, err error)
就是一个空接口,如果现在去实现这么一个功能,就可以这样做
switch i.(type){
case int: fmt.Printf("%d\n",i.(int))
case float64: fmt.Printf("%.2f\n",i.(float64))
case string: fmt.Printf("%s\n",i.(string))
default: fmt.Printf("Unknown\n")
}
GO 库中存在很多接口,io.Reader,io.Writer,fmt.Stringer,sort.Interface,http.Handler
,将在X.5做简单介绍。