首页 > 编程笔记

Go语言接口的定义和使用(超级详细)

接口在 Go 语言中有着举足轻重的地位,接口是一个编程约定的协议,也是一组方法签名的集合。

Go 语言的接口是非侵入式的设计,也就是说,一个具体类型实现接口不需要在语法上显式地声明,只要具体类型的方法集是接口方法集的超集,就代表该类型实现了接口,编译器在编译时会进行方法集的校验。接口是没有具体实现逻辑的,也不能定义字段。

接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。

接口的声明(定义)

Go语言的接口分为接口字面量类型和接口命名类型,接口的声明使用 interface 关键字。

1) 接口字面量类型的声明语法格式如下:
interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
    ...
}

2) 接口命名类型使用 type 关键字声明,语法格式如下:
type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
    ...
}
type writer interface{
    Write([]byte) error
}
使用接口字面量的情况很少,一般只有空接口 interface{} 类型变量的声明才会使用。

接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型的匿名字段,还可以是二者的混合。接口支持嵌入匿名接口字段,就是一个接口定义中可以包括其他接口,Go语言编译器会自动进行展开处理,类似于 C语言中宏的概念。

常见的接口及写法如下:
type Reader interface {
   Read (p []byte) (n int, err error)
}
type Writer interface {
   Write (p []byte) (n int,err error
}
//以下3种声明是等价的,最终的展开模式都是第3种格式
type Read Writer interface {
   Reader
   Writer
}
type Readwriter interface {
   Reader
   Write(P []byte) (n int, err error)
}
type ReadWriter interface {
   Read(p []byte) (n int,err error)
   Write(p []byte)(n int, err error)
}
声明接口类型的特点有以下几点:

接口初始化

单纯声明一个接口变量是没有任何意义的,接口只有被初始化为具体的类型时才有意义。

接口作为一个抽象层,起到抽象和适配的作用,没有初始化的接口变量,其默认值为 nil,例如:
var i io.Reader
fmt.Printf("%T\n",i)  //<nil>
接口绑定具体类型的实例的过程称为接口初始化。接口变量支持两种初始化方法,具体方法如下:

1、实例赋值接口

如果具体类型实例的方法集是某个接口的方法集的超集,则称该具体类型实现了接口,可以将该具体类型的实例直接赋值给接口类型的变量,此时编译器会进行静态的类型检查。

接口被初始化后,调用接口的方法就相当于调用接口绑定的具体类型的方法,这就是接口调用的语义。

2、接口变量赋值接口变量

已经初始化的接口类型变量 a 直接赋值给另一种接口变量 b,要求 b 的方法集是 a 的方法集的子集。此时 Go 编译器会在编译时进行方法集静态检查。这个过程也是接口初始化的一种方式,此时接口变量 b 绑定的具体实例是接口变量 a 绑定的具体实例的副本。

例如:
file, _ := os. OpenFile ("notes. txt", os.O_RDWR | os. O_CREATE, 0755)
var rw io. ReadWriter = file
//io. ReadWriter接口可以直接赋值给io.Writer接口变量
var W io.Writer = rw

接口的方法调用

接口方法调用与普通的函数调用是有区别的。

接口方法调用的最终地址是在运行时决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例的方法。

接口方法调用不是一种直接的调用,有一定的运行时开销。

直接调用未初始化的接口变量的方法会产生 panic,例如:
package main
type Printer interface {
   Print ()
}
type S struct{}
func (s S) Print() {
   println("print")
}
func main() {
   var i Printer
   //没有初始化的接口调用其方法会产生panic
   //panic: runtime error: invalid memory address or nil pointer dereference
   //i. Print()
   //必须初始化
   i = S{ }
   i.Print ()
}

1、接口的方法与实现接口的类型方法格式一致

在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。

也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。

为了抽象数据写入的过程,定义 DataWriter 接口来描述数据写入需要实现的方法,接口中的 WriteData() 方法表示将数据写入,写入方无须关心写到哪里。实现接口的类型实现 WriteData() 方法时,会具体编写将数据写入什么结构中。这里使用 file 结构体实现 DataWriter 接口的 WriteData() 方法,方法内部只是打印一个日志,表示有数据写入,例如:
package main
import (
    "fmt"
)
//定义一个数据写入器
type DataWriter interface {
    WriteData(data interface{}) error
}
//定义文件结构,用于实现DataWriter
type file struct {
}
//实现DataWriter接口的WriteData方法
func (d *file) WriteData(data interface{}) error {
    //模拟写入数据
    fmt.Println("WriteData:", data)
    return nil
}
func main() {
    //实例化file
    f := new(file)
    //声明一个DataWriter的接口
    var writer DataWriter
    //将接口赋值f,也就是*file类型
    writer = f
    //使用DataWriter接口进行数据写入
    writer.WriteData("data")
}
运行结果如下:

WriteData: data

在以上代码中:
当类型无法实现接口时,编译器会报错,下面列出几种常见的接口无法实现的错误。

1) 函数名不一致导致的错误

在以上代码的基础上尝试修改部分代码,造成编译错误,通过编译器的报错理解如何实现接口的方法。

例如,修改 file 结构的 WriteData() 方法名,将这个方法签名(第 13 行)修改为:
func (d *file) WriteDataX(data interface{}) error {
编译器报错,如下图所示:


图 1 函数名不一致

报错的位置在第 24 行。报错含义如下:不能将 f 变量(类型 *file)视为 DataWriter 进行赋值。原因如下:*file 类型未实现 DataWriter 接口(丢失 WriteData() 方法)。

WriteDataX() 方法的签名本身是合法的,但编译器扫描到第 24 行代码,发现尝试将 *file 类型赋值给 DataWriter 时,需要检查 *file 类型是否完全实现了 DataWriter 接口。显然,编译器因为没有找到 DataWriter 需要的 WriteData() 方法而报错。

2) 实现接口的方法签名不一致导致的报错

将修改的代码恢复后,再尝试修改 WriteData() 方法,把 data 参数的类型从 interface{} 修改为 int 类型,代码如下:
func (d *file) WriteData(data int) error {
编译器报错,如下图所示:


图 2 实现接口的方法签名不一致

这次未实现 DataWriter 的理由变为(错误的 WriteData() 方法类型)发现 WriteData(int)error,期望 WriteData(interface{})error。

这种方式的报错就是由实现者的方法签名与接口的方法签名不一致导致的。

2、接口中所有方法均被实现

当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。

为 DataWriter 添加一个方法,代码如下:
//定义一个数据写入器
type DataWriter interface {
    WriteData(data interface{}) error
    //能否写入
    CanWrite() bool
}
新增 CanWrite() 方法,返回 bool。此时再次编译代码,报错如下:
cannot use f (type *file) as type DataWriter in assignment:
        *file does not implement DataWriter (missing CanWrite method)
需要在 file 中实现 CanWrite() 方法才能正常使用 DataWriter()。

Go 语言的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计被称为非侵入式设计。

实现者在编写方法时,无法预测未来哪些方法会变为接口。一旦某个接口创建出来,要求旧的代码来实现这个接口时,就需要修改旧的代码的派生部分,这一般会导致代码的重新编译。

推荐阅读