[Journey with golang] 4. Interface

golang的接口与我已有的认知不太一样,这篇文章要写好一阵子。


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

接口变量只有值和类型的概念,所以接口类型变量仍然称为接口变量,接口内部存放的具体类型变量被称为接口指向的“实例”。接口只有声明没有实现,所以定义一个新接口,通常又变成声明一个新接口,二者通用,意思相同。

最常使用的接口字面量类型就是空接口 interface{} ,由于空接口的方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或传递给空接口,包括未命名类型的实例。注意:未命名类型由于不能定义自己的方法,所以方法集为空,因此其类型变量除了传递给空接口,不能传递给任何其他接口。

golang的接口分为接口字面量类型和接口命名类型,接口声明使用 interface 关键字。接口字面量类型声明语法如下所示:

interface{
    MethodSignature_1
    MethodSignature_2
}

接口命名类型使用 type 关键字声明,语法如下所示:

type InterfaceName interface{
    MethodSignature_1
    MethodSignature_2
}

使用接口字面量的场景很少,一般只有空接口interface{}类型变量的声明才会使用。

接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型匿名字段,还可以是二者的混淆。例如:

// Reader ...
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Writer ...
type Writer interface {
    Write(p []byte) (n int, err error)
}

type readAndWrite interface {
    Reader
    Writer
}

type readAndWrite2 interface {
    Read(p []byte) (n int, err error)
    Writer
}

golang的函数没有“函数声明”,类型的方法本质上就是函数的一种特殊形式,但golang有“方法声明”,而不是使用“方法签名”。严格意义上的函数签名是函数的字面量类型,函数签名是不包括函数名的,而函数声明是指带上函数名的函数签名。同理,接口定义使用方法声明,而不是方法签名。可以说,方法声明=方法名+方法签名。golang编译器在做接口匹配判断时是严格校验方法名和方法签名的。

声明新接口类型的特点:

  1. 接口的命名一般以er结尾
  2. 接口定义的内部方法声明不需要用 func 来引导
  3. 在接口定义中,只有方法声明而没有方法实现

接口只有被初始化为具体的类型时才有意义。接口作为一个胶水层,起到抽象和适配的作用。没有初始化的接口变量,其默认值为nil。接口绑定具体类型的实例的过程称为接口初始化。接口变量支持两种直接初始化方法:实例赋值接口和接口变量复制接口变量。

如果具体类型实例的方法集是某个接口的方法集的超集,则称该具体类型实现了接口,可以将该具体类型的实例直接赋值给接口类型的变量,此时编译器会进行静态的类型检查。接口被初始化后,调用接口的方法就相当于调用接口绑定的具体类型的方法,这就是接口调用的语义。

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

接口方法调用和普通的函数调用有区别。接口方法调用的最终地址是在运行期决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例的方法。接口方法调用不是一种直接的调用,有一定的运行时开销。直接调用未初始化的接口变量的方法会引起panic,例如:

package main

type printer interface {
    print()
}

type s struct{}

func (ss s) print() {
    println("print successfully")
}

func main() {
    var i printer // this is an interface example
    // the following code will cause a panic
    // i.print()
    i = s{}   // an interface example must be inited, or it will cause panic
    i.print() // call function print via interface example
    var a s
    a.print() // call function print via struct example
}

接口分为动态类型接口和静态类型接口。接口绑定的具体实例的类型称为接口的动态类型。接口可以绑定不同类型的实例,所以接口的动态类型是随着其绑定的不同类型实例而发生变化的。若接口被定义时,其类型就已经被确定,这个类型叫接口的静态类型。接口的静态类型在其定义时就被确定,静态类型的本质特征就是接口的方法签名集合。两个接口如果方法签名集合相同(顺序可以不同),则这两个接口在语义上完全等价,它们之间不需要强制类型转换就可以相互赋值。原因是golang编译器校验接口是否能赋值,是比较二者的方法集,而不是看具体类型接口类型名。

相关推荐