本文目录:
- 鸭子类型
- 接口介绍
- 接口类型
- 实现接口
- 空接口
- 接口值
- 类型断言
鸭子类型
在介绍Go语言中的接口之前,我想给大家介绍一个有意思的概念:鸭子类型。
在程序设计当中,鸭子类型(duck typing)是动态类型的一种风格。
在这种风格中,一个对象的有效的语义,不是由继承自特定的类或者实现特定的接口,而是由"当前方法和属性的集合"决定的。
这个概念的名字来源于James Whitcomb Riley提出的"鸭子测试",原文翻译过来是这样的:
当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
在鸭子类型中,关注点在于对象的行为,能做什么;而不是关注对象所属的类型。
例如,在不使用鸭子类型的语言(JAVA、C++)中,我们可以编写一个函数,它接受一个类型为"鸭子"的对象,并调用它的"走"和"叫"方法。在使用鸭子类型的语言(Go、Python)中,这样的一个函数可以接受一个任意类型的对象,并调用它的"走"和"叫"方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的"走"和"叫"方法的对象都可被函数接受的这种行为引出了以上表述,这种由行为决定类型的方式因此得名。
接口介绍
Go语言接口的独特之处在于它是隐式实现的,是鸭子类型。对于一个具体的类型,无须声明它实现了哪些接口,只要提供接口所必须的方法即可。
接口是一种抽象类型。它不包含数据布局或者内部结构,仅提供方法。
接口类型
一个接口类型定义了一套方法,如果一个具体类型要实现该接口,那么必须实现接口类型定义中的所有方法。
例如,io.Writer是一个广泛使用的接口,它负责所有可以写入字节的类型的抽象,包括文件、内存缓冲区、网络连接、HTTP客户端、打包器(archiver)、散列器(hasher)等。io包还定义了很多有用的接口。Reader就抽象了所有可以读取字节的类型,Closer抽象了所有可以关闭的类型,比如文件和网络连接。
package io
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
看到这里,你应该注意到了Go语言中单方法接口的命名约定了。
另外,我们还可以通过组合已有的接口得到新的接口:
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
上面这种语法被称为嵌入式接口。与嵌入式结构类似,我们可以直接使用一个接口,而不用逐一写出这个接口所包含的方法。
最后,接口中方法定义的顺序不影响接口的类型,真正影响接口类型的只有接口的方法集合。
实现接口
如果一个类型实现了一个接口中的所有方法,那么这个类型就实现了这个接口。
例如,*os.File类型实现了Read()方法、Write()方法和Close()方法,因此,*os.File类型实现了io.Reader、io.Writer、io.Closer和io.ReadWriter等接口。
与*os.File类型有所区别的*buffer.Buffer没有实现Close()方法,因此,*buffer.Buffer实现了io.Reader、io.Writer和io.ReadWriter接口,没有实现io.Closer接口。
为了简化表述,我们通常说一个具体类型是一个(is-a)特定的接口类型,这意味着该具体类型实现了该接口。比如,bytes.Buffer是一个io.Writer;*os.File是一个io.ReadWriter
接口的赋值规则很简单,仅当一个表达式实现了一个接口时,这个表达式才可以赋给该接口。
var w io.Writer
w = os.Stdout // OK:*os.File有Write方法
w = new(bytes.Buffer) // OK:*bytes.Buffer有Write方法
w = time.Second // 编译错误:time.Duration缺少Write方法
var rwc io.ReadWriteCloser
rwc = os.Stdout // OK:*os.File有Read、Write和Close方法
rwc = new(bytes.Buffer) // 编译错误:*bytes.Buffer缺少Close方法
当右侧表达式也是一个接口时,该规则也有效:
w = rwc // OK:io.ReadWriteCloser有Write方法
rwc = w // 编译错误:io.Writer缺少CLose方法
空接口
一个拥有更多方法的接口,给了我们更多的信息,当然也提高了实现它的门槛。那么对于接口类型interface{},它完全不包含任何方法,通过这个接口能得到对应具体类型的什么信息呢?
由于interface()没有定义任何方法,所以通过这个接口得到的具体类型什么信息也获取不到。
看起来这个接口没有任何用途,但实际上被称为空接口类型的interface{}时不可缺少的。
正因为空接口类型对其实现类型没有任何要求,所以我们可以把任何值赋给空接口类型。
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)
当我们创建了一个指向布尔值,浮点数,字符串,map,指针或者其他类型的interface{}接口,也无法使用其中的值,因为这个空接口不包含任何方法。
我们需要使用类型断言从空接口中还原出实际值。
接口值
一个接口类型的值(简称接口值)由两个部分组成:一个具体类型和该类型的一个值。二者称为接口的动态类型和动态值。
由于Go语言时一门静态类型语言,类型是编译时的概念,所以类型不是一个值。通常,我们用类型描述符来描述每个类型的具体信息,比如类型的名称和方法。对于一个接口值,类型部分就用对应的类型描述符来表述。
举个例子就能很好的理解动态类型与动态值的概念了:
var w io.Writer = os.Stdout
在这个例子中,我们声明了一个io.Writer接口类型的变量w,并将其赋值为os.Stdout对象,我们查看以下os.Stdout是个什么样的对象,其在os包的file.go中有如下定义:
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
func NewFile(fd uintptr, name string) *File {
// ...省略不重要的部分
return newFile(h, name, "file")
}
func newFile(h syscall.Handle, name string, kind string) *File {
// ...省略不重要的部分
f := &File{&file{
// ...省略不重要的部分
}}
return f
}
一层一层的展开之后,我们发现:os.Stdout本质上就是一个*os.File对象,而os.File类型又通过实现了Write()方法来实现io.Writer接口。
所以,我们才可以将os.Stdout对象赋值给io.Writer接口对象w。
此时w的动态类型就是*os.File这个具体类型,动态值为os.Stdout这个具体对象。
接口值可以用==和!=操作符来做比较。如果两个接口值都是nil或者动态类型完全一致且动态值相等,那么两个接口值相等。
因为接口值是可以比较的,所以他们可以作为map的键,也可以作为switch语句的操作数。
需要注意的是,当两个接口动态类型一致,但动态值是不可比较的(比如slice),这个时候比较两个接口对象将会引起程序崩溃。
我们可以通过fmt包中%T来拿到接口的动态类型。
var w io.Writer
fmt.Printf("%T\n", w) // "<nil>"
w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"
w = new(bytes.Buffer) // "*bytes.Buffer"
fmt.Printf("%T\n", w)
类型断言
类型断言是一个作用在接口值上的操作,语法上写为x.(T),其中x是一个接口类型,T是一个类型(断言类型)。T既可以是具体类型,也可以是接口类型。
类型断言会检查作为操作数的动态类型是否满足指定的断言类型。
当T是具体类型的时候,类型断言会检查x的动态类型是否就是T。如果检查成功,类型断言的结果就是x的动态值,类型就是T。如果检查失败,那么操作崩溃。也就是说,在检查成功的情况下,类型断言的操作就是将接口的动态值取出来。
当T是接口类型的时候,类型断言会检查x的动态类型是否满足T接口。如果检查成功,与具体类型不同,不会提取动态值,仅将结果的类型变为接口类型T。也就是说,类型断言的操作是将x进行了一次接口类型转换(通常是转换为方法更多的接口),但保留了接口值中的动态类型与动态值的部分。
当我们不确定接口值的动态类型,又想检测的时候,可以通过如下代码:
var w io.Writer = os.Stdout
f, ok := w.(*os.File) // 成功:f == os.Stdout
b, ok := w.(*bytes.Buffer) // 失败:b == nil
用上面的这种语句来做接口的动态类型检测,在断言失败的情况下不会引起程序崩溃,而是会多返回一个布尔类型的返回值来表示断言是否成功。
通常,我们用if表达式的扩展形式,用一段比较紧凑的代码形式来使用类型断言:
if f, ok := w.(*os.File); ok {
// 断言成功
}
当类型断言的操作数是一个变量是,有时你会看到返回值的名字与操作数的变量名一致,原有的值被新的返回值覆盖了:
if w, ok := w.(*os.File); ok {
// 断言成功,w为*os.File类型
}