接口
接口是一个编程规约,一组方法签名的组合。接口是没有具体实现的逻辑,不能定义字段
- 变量和实例
实例里蕴含了变量值、变量类型和附着在类型上的方法等语义。实例和面向对象编程中的对象改了类似,我们使用实例来代表具体类型的变量,接口变量只有值和类型的概念,所有接口类型变量仍然称为接口变量,接口内部存放的具体类型变量被称为接口指向的实例。接口只有声明没有实现,所有定义一个新接口,通常又变成声明一个新接口,定义接口和声明接口两者代表相同的意思。
- 空接口
最常使用的接口字面量类型就是空接口 interface{}
,由于空接口的方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或传递给空接口,包括非命名类型实例。
非命名类型由于不能定义自己的方法,所以方法集为空,因此其类型变量除了传递给空接口,不能传递给任何其他接口。
接口声明
- 接口字面量类型的声明语法
1 | interface { |
- 接口命名类型使用type类型关键字声明语法
1 | type InterfaceName interface { |
使用接口字面量场景很少,一般只有空接口类型变量的声明才会使用。
接口支持嵌入匿名接口字段,就是一个接口定义里可以包括其他接口,Go 编译器会自动进行展开处理,有点类似C语言中的宏的概念。
1 | type Reader interface { |
方法声明
1 | // 方法声明 = 方法名 + 方法签名 |
接口中的方法声明非常类似于C语言的函数声明概念,Go 编译器在做接口匹配判断时是杨校验方法名称和方法签名的。
声明新接口类型的特点
- 接口命名一般以”er”结尾
- 接口定义内部方法声明不需要 func 引导
- 接口定义中,方法声明没有方法实现
接口初始化
单纯声明一个接口变量没有任何意义,接口只有被初始化为具体的类型时才有意义。接口作为一个抽象层,起到抽象和适配的作用。没有初始化的接口变量,默认值为nil。
1 | var i io.Reader |
接口绑定具体类型的实例的过程称为接口初始化。接口变量支持两种直接的初始化方法。
- 实例赋值接口
- 接口变量赋值接口变量
1 | file, _ := os.Openfile("notes.txt", os.O_RDWR|os.O_CREATE, 0755) |
接口方法调用
接口方法调用不同于普通函数调用。接口方法调用最终地址是在运行时期决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接调用实例的方法。接口方法调用不是一种直接的调用,有一定的运行时开销。
直接调用未初始化的接口变量方法会出现 panic 。
1 | package main |
接口的动态类型和静态类型
- 动态类型
接口绑定的具体实例为接口的动态类型。接口可以绑定不同类型的实例,所有接口的动态类型是随着其绑定的不同类型实例而发生变化的。
- 静态类型
接口被定义时,其类型就已经被确定,这个类型叫接口的静态类型。静态类型的本质特征就是接口的方法签名集合。两个接口如果方法签名集合相同(方法的顺序可以不同),则这两个接口在语义上完全等价,它们之间不需要强制类型转换就可以直接赋值。
接口运算
类型断言
接口类型断言的语法形式如下:
1 | i.(TypeName) |
i 必须是接口变量,如果是具体类型变量,则编译器会报错,TypeName 可以是接口类型名,也可以是具体类型名。
接口查询的两层语义
如果 TypeName 是一个具体类型名,则类型断言用于判断接口变量 i 绑定的实例类型是否就是具体类型 TypeName。
如果 TypeName 是一个接口类型名,则类型断言用于判断接口变量 i 绑定的实例类型是否同时实现了 TypeName 接口。
接口断言的两种语法实现
直接赋值模式如下:
1 | o := i.(TypeName) |
语义分析:
- TypeName 是具体类型名,此时如果接口 i 绑定的实例类型就是具体类型 TypeName,则变量 o 的类型就是 TypeName,变量 o 的值就是接口绑定的实例值的副本(当然实例可能是指针值,那就是指针值的副本)
- TypeName 是接口类型名,如果接口 i 绑定的实例类型满足接口类型 TypeName,则变量 o 的类型就是接口类型 TypeName,o 底层绑定的具体类型实例 i 绑定的实例的副本(当然实例可能是指针值,那就是指针值的副本)。
- 如果上述两种情况都不满足,则程序抛出 panic
1 | package main |
comma,ok 表达模式:
1 | if o, ok := i.(TypeName); ok { |
- TypeName 是具体类型名,此时如果接口 i 绑定的实例类型就是具体类型 TypeName 则 ok 为 True,变量 o 的类型就是 TypeName,变量 o 的值 就是接口绑定实例值的副本
TypeName 是接口类型名,如果接口 i 绑定的实例类型满足接口类型 TypeName,则 ok 为True,变量 o的类型就是接口类型TypeName,o底层绑定的具体类型实例是 i 绑定的实例的副本
如果上述都不满足,则 ok 为 false,变量 o 是 TypeName类型的零值。
1 | package main |
类型查询
- i 必须是接口类型 (接口未初始化在返回 nil)
- case 字句后可以跟非接口类型名
1 | f, err := os.OpenFile("xxx", os.O_RDWR|os.O_CREATE) |
- 如果 case 后面是具体类型名,且接口变量 i 绑定的实例类型和该具体类型相同,则匹配成功, v 为具体类型变量,v 的值是 i 绑定实例值的副本
1 | f, err := os.OpenFile("xxx", os.O_RDWR|os.O_CREATE) |
- 如果 case 后面跟多个类型,用逗号分隔,接口变量 i 绑定的实例类型只要和其中一个类型匹配,则直接使用 o 赋值给 v,相当于
v := o
1 | f, err := os.OpenFile("xxx", os.O_RDWR|os.O_CREATE) |
如果 所以case 不满足,则执行 default 语句,此时执行的仍然是
v := o
,最终 v 的值是 o。此时使用 v 没有任何意义fallthrough 语句不能在该 Type Switch 语句中使用。
不推荐用法:
1 | switch i := i.(type) {} |
推荐用法:
1 | switch v := i.(type) {} |
- 类型查询和断言有相同的语义只是语法格式不同。
- 类型查询使用 case 判断,当然类型断言也可以使用 if 判断打到同样效果。
接口的优点和使用形式
- 优点:解耦、实现泛型
- 接口使用形式
- 作为结构内嵌字段
- 作为函数或方法的形参
- 作为函数或方法的返回值
- 作为其他接口定义的嵌入字段
空接口
Go 语言没有泛型,如果一个喊需要接收任意类型的参数,则参数类型可以使用空接口类型,这是没有弥补泛型的一种手段。例如:
1 | func Fprint(w io.Writer, a ...interface{}) (n int, err error) |
空接口与 nil
1 | package main |