1、接口声明
Go语言的接口分为接口字面量类型和接口命名类型,接口的声明使用interface关键字。
接口字面量类型的声明语法如下:
interface{
Methodsignature1
MethodSignature2
}
接口命名类型使用ype关键字声明,语法如下:
type InterfaceName interface {
Methodsignature1
Methodsignature2
}
使用接口字面量的场景很少,一般只有空接口interface类型变量的声明才会使用。
接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型匿名字段,还可以是二者的混合。接口支持嵌入匿名接口字段,就是一个接口定义里面可以包括其他接口,Go编译器会自动进行展开处理,有点类似C语言中宏的概念。例如:
package main
func main() {
}
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
//如下3种声明是等价的,最终的展开形式都是第3种
type ReadWriter 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)
}
方法声明
由函数和类型系统章节可知:Go中的函数没有使用“函数声明”,类型的方法本质上就是函数的一种特殊形式。我们提到了“方法声明”的概念,而不是使用“方法签名”。这里有必要澄清一下这两个概念:严格意义上的函数签名是函数的字面量类型,函数签名是不包括函数名的,而函数声明是指带上函数名的函数签名。同理,对于方法也是一样,接口定义使用方法声明,而不是方法签名,因为方法名是接口的组成部分
//方法声明=方法名+方法签名
MethodName (InputTypeList) OutputTypeList
接口中的“方法声明”非常类似于C语言中的函数声明的概念,Go编译器在做接口匹配判断时是严格校验方法名称和方法签名的。
声明新接口类型的特点
- 接口的命名一般以"er"结尾
- 接口定义的内部方法声明不需要func引导
- 在接口定义中,只有方法声明没有方法实现。
2、接口初始化
单纯的声明一个接口变量没有任何意义,接口只有被初始化为具体的类型时才有意义。接口作为一个胶水层或抽象层,起到抽象和适配的作用。没有初始化的接口变量,其默认值是nil。
var i io.Reader
fmt.Printf("%T\n", i)//nil
接口绑定具体类型的实例的过程称为接口初始化。接口变量支持两种直接初始化方法,具体如下。
实例赋值接口
如果具体类型实例的方法集是某个接口的方法集的超集,则称该具体类型实现了接口,可以将该具体类型的实例直接赋值给接口类型的变量,此时编译器会进行静态的类型检查。接口被初始化后,调用接口的方法就相当于调用接口绑定的具体类型的方法,这就是接口调用的语义。
接口变量赋值接口变量
已经初始化的接口类型变量a直接赋值给另一种接口变量b,要求b的方法集是a的方法集的子集。此时Go编译器会在编译时进行方法集静态检查。这个过程也是接口初始化的一种方式,此时接口变量b绑定的具体实例是接口变量a绑定的具体实例的副本。例如:
file := os.OpenFile("notes.txt",os.O_RDWRIOs.O_CREATE,0755)
var rw io.ReadWriter = file
//io.ReadWriter接口可以直接赋值给io.Writer接口变量
var w io.Writer = rw
接口方法调用
接口方法调用和普通的函数调用是有区别的。接口方法调用的最终地址是在运行期决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例的方法。接口方法调用不是一种直接的调用,有一定的运
行时开销(直接调用未初始化的接口变量的方法会产生panic。例如:
package main
func main() {
var i Printer
//没有初始化的接口调用其方法会产生panic
//panic: runtime error: invalid memory address or nil pointer dereference
//i.Print()
//必须初始化
i = S{}
i.Print()
}
type Printer interface {
Print()
}
type S struct{}
func (s S) Print() {
println("print")
}
4、接口的动态类型和静态类型
动态类型
接口绑定的具体实例的类型称为接口的动态类型。接口可以绑定不同类型的实例,所以接口的动态类型是随着其绑定的不同类型实例而发生变化的。
静态类型
接口被定义时,其类型就已经被确定,这个类型叫接口的静态类型。接口的静态类型在其定义时就被确定,静态类型的本质特征就是接口的方法签名集合。两个接口如果方法签名集合相同(方法的顺序可以不同),则这两个接口在语义上完全等价,它们之间不需要强制类型转换就可以相互赋值。原因是Go编译器校验接口是否能赋值,是比较二者的方法集,而不是看具体接口类型名。a接口的法集为A,b接口的法集为B,如果B是A的子集合,则a的接口变量可以直接赋值给B的接口变量。