目录

    1. 面向接口编程

    1.1 特征

    面向接口编程,强调的是模块之间通过接口进行交互。首先,调用方指定一组方法签名,然后,由被调用方实现这组方法。

    接口编程与其他编程显著不同的一点是,接口编程关注的是方法,而不是属性。在很多的编程场景中,方法是围绕属性进行定义的。如下图:

    但在接口编程中,恰好相反,方法处于核心位置,而属性可以自由定义,进行扩展。在不同的数据结构上,实现同一个接口。如下图:

    此外,从文字上也可以看到,面向接口编程是以接口为中心的编程范式。

    1.2 优势

    • 模块解耦

    通过接口抽象,明确模块之间的依赖关系。模块与模块之间的边界更加清晰,各个模块也更加独立。

    • 可维护性

    Interface 隐藏了具体实现,各个模块只需要保持 Interface 不变,内部逻辑可以自由重构。

    • 易扩展、易升级

    只需要实现相同的一组方法,就可以很方面地对模块进行扩展、升级。

    • 易测试

    在写单元测试时,需要屏蔽外部依赖。而 Interface 非常容易 Mock 。面向接口编程的代码,更加容易编写单元测试。

    2. Go 中的 Interface

    Go 中的 Interface 是一种聚合了一组方法的类型。

    2.1 声明和实现

    通过 interface 关键字,加上一组方法签名,就可以声明一个接口。方法签名指的是,方法的名字、参数、返回值等能够唯一确定一个方法的全部信息。下面声明一个 Animal 的接口:

    type Animal interface{
        Call()
    }
    

    Go 中的 Interface 是隐式实现的。不需要指定继承了哪一个 Interface ,只需要在同一个 package 中,某个类型实现了 Interface 的全部方法,就称这个类型为 Interface 的实现。下面这个例子的 Cat 类型实现了 Animal 接口:

    type Cat struct {}
    
    func (c Cat) Call() {
        // do something
    }
    

    在使用时,可以声明一个 Aminal 类型的变量,指向 Cat之后,调用接口中的方法。

    var i Animal
    i = &Cat{}
    i.Call()
    

    2.2 Receiver 类型

    Go 中可以定义 type 上的方法,Receiver 指的就是这个方法接受的类型,例如示例中的 Cat 。

    上面的示例中,使用 i = &Cat{}i = Cat{} 都是可以的,Go 会自动实现转换。除了 Receiver 为指针类型,而赋值为值类型。因为 Go 采用的是值传递,赋值之后,取得的地址指向的是拷贝的地址,而不是预期的数据地址,编译时会报错:

        Cat does not implement Animal (Call method has pointer receiver)
    

    内置的类型不能作为 Receiver 。Receiver 分为两种类型,值和指针,也可以混合使用。

    • 值类型的 Receiver
    type Dog struct {
        times int
    }
    
    func (d Dog) Call() {
        d.times = d.times + 1 // not work
    }
    
    • 指针类型的 Receiver
    type Dog struct {
        times int
    }
    
    func (d *Dog) Call() {
        d.times = d.times + 1 // work
    }
    

    在实现接口时,会遇到这两种类型的选择。传递指针,似乎更加高效,但也意味着操作更危险。在不需要修改数据的场景中,应该尽量采用值 Receiver 。

    2.3 空接口

    空接口的特殊性在于,interface{} 可以接受任意类型的值。这个特性看着像是动态语言才有的,但 Go 是一个静态语言。在编译时,Go 会对 Interface 进行严格校验。

    package main
    
    import "fmt"
    
    func main() {
        var i interface{}
        i = 123
        fmt.Println(i)
        i = "123"
        fmt.Println(i)
    }
    

    空接口在接受或者返回不确定类型参数时,非常有用。但不确定的类型,也会带来维护的成本。在项目中,我们应该尽量避免使用空接口,以增强项目的可维护性。

    2.4 类型判断

    由于 interface{} 可以接受任意类型的值,在程序运行过程中,很有可能需要知道 Interface 的动态值类型。有两种方法,可以判断类型:

    • 断言
    var i interface{}
    i = Cat{}
    if _, ok := i.(Cat); ok {
        fmt.Println("i is Cat ")
    } else {
        fmt.Println("i is not Cat ")
    }
    
    • switch
    var i interface{}
    i = Dog{}
    switch i.(type) {
    case Cat:
        fmt.Println("i is Cat")
    case Dog:
        fmt.Println("i is Dog")
    default:
        fmt.Println("i is unknow")
    }
    

    需要注意的是,这里的 Cat 和 Dog 值对 Animal 做类型判断时,为 true 。

    3. Interface 的组合

    type Active interface{
        Eat()
    }
    
    type Animal interface{
        Active
        Call()
    }
    
    type Cat struct {}
    
    func (c Cat) Call() {}
    
    func (c *Cat) Eat() {}
    

    Interface 中,还可以嵌套其他 Interface 。实现这个 Interface 的类型,需要同时提供全部 Interface 定义的方法。这种技巧在项目中,非常有用。