Go基础:接口的基础知识介绍

233 阅读8分钟

本文目录:

  • 鸭子类型
  • 接口介绍
  • 接口类型
  • 实现接口
  • 空接口
  • 接口值
  • 类型断言

鸭子类型

在介绍Go语言中的接口之前,我想给大家介绍一个有意思的概念:鸭子类型

在程序设计当中,鸭子类型(duck typing)是动态类型的一种风格

在这种风格中,一个对象的有效的语义,不是由继承自特定的类或者实现特定的接口,而是由"当前方法和属性的集合"决定的。

这个概念的名字来源于James Whitcomb Riley提出的"鸭子测试",原文翻译过来是这样的:

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

在鸭子类型中,关注点在于对象的行为,能做什么;而不是关注对象所属的类型。

例如,在不使用鸭子类型的语言(JAVA、C++)中,我们可以编写一个函数,它接受一个类型为"鸭子"的对象,并调用它的"走"和"叫"方法。在使用鸭子类型的语言(GoPython)中,这样的一个函数可以接受一个任意类型的对象,并调用它的"走"和"叫"方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的"走"和"叫"方法的对象都可被函数接受的这种行为引出了以上表述,这种由行为决定类型的方式因此得名。

接口介绍

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.Readerio.Writerio.Closerio.ReadWriter等接口。

*os.File类型有所区别的*buffer.Buffer没有实现Close()方法,因此,*buffer.Buffer实现了io.Readerio.Writerio.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类型
}