Go第十三课-接口

117 阅读5分钟
  1. 接口类型是由 type 和 interface 关键字定义的一组方法集合,其中,方法集合唯一确定了这个接口类型所表示的接口。

    type MyInterface interface {
        M1(int) error
        M2(io.Writer, ...string)
    }
    
  2. 接口类型声明中的方法必须是具名的,并且方法名字在这个接口类型的方法集合中是唯一的。

  3. 在 Go 接口类型的方法集合中放入首字母小写的非导出方法也是合法的。

  4. 类型为接口类型的变量被称为接口类型变量,如果没有被显式赋予初值,接口类型变量的默认值为 nil。

  5. 如果一个类型 T 的方法集合是某接口类型 I 的方法集合的等价集合或超集,我们就说类型 T 实现了接口类型 I,那么类型 T 的变量就可以作为合法的右值赋值给接口类型 I 的变量。

  6. 接口类型定义中嵌入的不同接口类型的方法集合若存在交集,交集中的方法不仅名字要一样,函数签名也要相同。

  7. Go 语言还支持接口类型变量赋值的“逆操作”,也就是通过接口类型变量“还原”它的右值的类型与值信息,这个过程被称为“类型断言(Type Assertion)”。

    v, ok := i.(T)
    
    var a int64 = 13
    var i interface{} = a
    v1, ok := i.(int64)
    fmt.Printf("v1=%d, the type of v1 is %T, ok=%t\n", v1, v1, ok) 
    // v1=13, the type of v1 is int64, ok=true
    
  8. 尽量定义“小接口”

    1. 隐式契约,无需签署,自动生效
    2. 更倾向于“小契约”:尽量定义小接口,即方法个数在 1~3 个之间的接口
  9. 小接口有哪些优势?

    1. 第一点:接口越小,抽象程度越高
    2. 第二点:小接口易于实现和测试
    3. 第三点:小接口表示的“契约”职责单一,易于复用组合
  10. 定义小接口遵循的几点:

    1. 首先,别管接口大小,先抽象出接口。
    2. 第二,将大接口拆分为小接口。
    3. 最后,我们要注意接口的单一契约职责。
  11. 接口是 Go 这门静态语言中唯一“动静兼备”的语法特性。

  12. 接口的静态特性体现在接口类型变量具有静态类型。拥有静态类型,那就意味着编译器会在编译阶段对所有接口类型变量的赋值操作进行类型检查。

  13. 接口的动态特性,就体现在接口类型变量在运行时还存储了右值的真实类型信息,这个右值的真实类型被称为接口类型变量的动态类型。

    var err error
    err = errors.New("error1")
    fmt.Printf("%T\n", err)  // *errors.errorString
    
  14. 所谓鸭子类型,就是指某类型所表现出的特性(比如是否可以作为某接口类型的右值),不是由其基因(比如 C++ 中的父类)决定的,而是由类型所表现出来的行为(比如类型拥有的方法)决定的。

  15. 尽量避免使用空接口作为函数参数类型。一旦使用空接口作为函数参数类型,你将失去编译器为你提供的类型安全保护屏障。

接口类型变量的内部表示

  1. 在运行时层面,接口类型变量有两种内部表示:ifaceeface,这两种表示分别用于不同的接口类型变量:

    1. eface 用于表示没有方法的空接口(empty interface)类型变量,也就是 interface{}类型的变量;
    2. iface 用于表示其余拥有方法的接口 interface 类型变量。
  2. 我们判断两个接口类型变量是否相同,只需要判断 _type/tab 是否相同,以及 data 指针指向的内存空间所存储的数据值是否相同就可以了。这里要注意不是 data 指针的值相同噢。

  3. 对于空接口类型变量,只有 _type 和 data 所指数据内容一致的情况下,两个空接口类型变量之间才能划等号。

  4. 但 Go 在进行等值比较时,类型比较使用的是 eface 的 _typeiface 的 tab._type,因此就像我们在这个例子中看到的那样,当 eif 和 err 都被赋值为T(5)时,两者之间是划等号的。

接口类型的装箱(boxing)原理

  1. 装箱(boxing)是编程语言领域的一个基础概念,一般是指把一个值类型转换成引用类型
  2. 接口类型的装箱实际就是创建一个 eface 或 iface 的过程。
  3. 装箱操作是由 Go 编译器和运行时共同完成的。

Go 接口的应用模式或惯例

  1. 如果 C++ 和 Java 是关于类型层次结构和类型分类的语言,那么 Go 则是关于组合的语言。

  2. 组合是 Go 语言的重要设计哲学之一,而正交性则为组合哲学的落地提供了更为方便的条件。正交性用于表示某种不相依赖性或是解耦性。

  3. 垂直组合:更多用于类型定义层面,本质上它是一种类型组合,也是一种类型之间的耦合方式。

    1. 第一种:通过嵌入接口构建接口

      // $GOROOT/src/io/io.go
      type ReadWriter interface {
          Reader
          Writer
      }
      
    2. 第二种:通过嵌入接口构建结构体类型

      type MyReader struct {
        io.Reader // underlying reader
        N int64   // max bytes remaining
      }
      
    3. 第三种:通过嵌入结构体类型构建新结构体类型

  4. 水平组合:接口可以将各个类型水平组合(连接)在一起,通过接口进行水平组合的基本模式就是:使用接受接口类型参数的函数或方法。

  5. Go 社区流传一个经验法则:“接受接口,返回结构体(Accept interfaces, return structs)”