go 接口

非侵入式设计

  • Go 语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现。而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现
  • 非侵入式设计是 Go 语言设计师经过多年的大项目经验总结出来的设计之道。只有让接口和实现者真正解耦,编译速度才能真正提高,项目之间的耦合度也会降低不少
  • 传统的派生式接口及类关系构建的模式,让类型间拥有强耦合的父子关系。这种关系一般会以“类派生图”的方式进行。经常可以看到大型软件极为复杂的派生树。随着系统的功能不断增加,这棵“派生树”会变得越来越复杂。对于 Go 语言来说,非侵入式设计让实现者的所有类型均是平行的、组合的。如何组合则留到使用者编译时再确认。因此,使用 Go 语言时,不需要同时也不可能有“类派生图”,开发者唯一需要关注的就是“我需要什么”,以及“我能实现什么”

接口定义

  • 接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构
  • 在面向对象中,接口定义了一个对象的行为
  • 使用 type 和 interface 关键字定义接口

    type interface_name interface {
    method_name1([param_list]) [return_type]
    method_name2([param_list]) [return_type]
    method_name3([param_list]) [return_type]
    ...
    method_namen([param_list]) [return_type]
    }
    
    • interface_name:接口类型名。使用 type 将接口定义为自定义的类型名。Go 语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer,有关闭功能的接口叫 Closer 等
    • method_name:方法名。当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包之外的代码访问
    • param_list return_type:参数列表和返回值列表中的参数变量名可以被忽略

接口实现

  • 接口类型是由一组方法签名定义的集合,接口类型的变量可以保存任何实现了这些方法的值
  • 接口把所有的具有共性的方法定义在一起,如果一个任意类型 T 的方法集为一个接口类型的方法集的超集,则说类型 T 实现了此接口类型。T 可以是一个非接口类型,也可以是一个接口类型
  • 实现关系在 Go 语言中是隐式的。两个类型之间的实现关系不需要在代码中显式地表示出来。Go 语言中没有类似于 implements 的关键字。Go 编译器将自动在需要的时候检查两个类型之间的实现关系
  • 接口定义后,需要实现接口,调用方才能正确编译通过并使用接口。接口的实现需要遵循两条规则才能让接口可用

    • 接口的方法与实现接口的类型方法格式一致
    • 接口中所有方法均被实现

      package main
      
      import (
      "fmt"
      "math"
      )
      
      type abser interface {
      Abs() float64
      }
      
      func main() {
      var a abser
      f := myFloat(-math.Sqrt2)
      v := vertex{3, 4}
      
      a = f // a myFloat 实现了 abser
      fmt.Println(a.Abs())
      
      a = &v // a *vertex 实现了 abser
      fmt.Println(a.Abs())
      
      // a = v // error: v 是一个 vertex(而不是 *vertex), 所以没有实现 abser
      }
      
      type myFloat float64
      
      func (f myFloat) Abs() float64 {
      if f < 0 {
      return float64(-f)
      }
      return float64(f)
      }
      
      type vertex struct {
      X, Y float64
      }
      
      func (v *vertex) Abs() float64 {
      return math.Sqrt(v.X*v.X + v.Y*v.Y)
      }
      

接口值

  • 接口也是值
    • 接口值可用作函数的参数或返回值
    • 在内部,接口值可看做包含值和具体类型的元组 (value, type)
    • 通过 %v%T 可以访问接口的值和类型
  • 接口值保存了一个具体底层类型的具体值
  • 接口值调用方法时会执行其底层类型的同名方法

    package main
    
    import (
    "fmt"
    "math"
    )
    
    type myInterface interface {
    M()
    }
    
    type st struct {
    S string
    }
    
    func (t *st) M() {
    fmt.Println(t.S)
    }
    
    type myFloat float64
    
    func (f myFloat) M() {
    fmt.Println(f)
    }
    
    func main() {
    var i myInterface
    
    i = &st{"Hello"}
    describe(i)
    i.M()
    
    i = myFloat(math.Pi)
    describe(i)
    i.M()
    }
    
    func describe(i myInterface) {
    fmt.Printf("(%v, %T)\n", i, i)
    }
    

底层值为 nil 的接口值

  • 接口内的具体值为 nil,方法仍然会被 nil 接收者调用。保存了 nil 具体值的接口本身并不为 nil

    package main
    
    import (
    "fmt"
    "math"
    )
    
    type myInterface interface {
    M()
    }
    
    type st struct {
    S string
    }
    
    func (t *st) M() {
    if t == nil {
        fmt.Println("<nil>")
        return
    }
    fmt.Println(t.S)
    }
    
    func main() {
    var i myInterface
    
    i = &st{"Hello"}
    describe(i)
    i.M()
    
    var stp *st
    i = stp
    describe(i)
    i.M()
    }
    
    func describe(i myInterface) {
    fmt.Printf("(%v, %T)\n", i, i)
    }
    

接口值为 nil

  • nil 接口值既不保存值也不保存具体类型
  • 为 nil 接口调用方法会报运行时错误,因为接口的元组内并未包含可以指明该调用哪个具体方法的类型

    package main
    
    import (
    "fmt"
    )
    
    type myInterface interface {
    M()
    }
    
    func main() {
    var i myInterface
    describe(i)
    i.M() // panic: runtime error: invalid memory address or nil pointer dereference
    }
    
    func describe(i myInterface) {
    fmt.Printf("(%v, %T)\n", i, i)
    }
    

空接口

  • 指定了零个方法的接口值称为 “空接口” interface{}
  • 空接口可保存任何类型的值(因为每个类型都至少实现了零个方法)
  • 空接口用于处理未知类型的值。如 fmt.Print 可接受类型为 interface{} 的任意数量的参数

    package main
    
    import "fmt"
    
    func main() {
    var i interface{}
    describe(i)
    
    i = 42
    describe(i)
    
    i = "hello"
    describe(i)
    }
    
    func describe(i interface{}) {
    fmt.Printf("(%v, %T)\n", i, i)
    }
    

类型断言(type assertion)

  • 类型断言提供了访问接口值底层具体值的方式 t := i.(T)
    • 断言接口值 i 保存了具体类型 T,并将其底层类型为 T 的值赋予变量 t
    • 若 i 未保存 T 类型的值,会触发一个 panic
  • 类型断言可返回两个值:其底层值,一个布尔值判断断言是否成功 t, ok := i.(T)

    package main
    
    import "fmt"
    
    func main() {
    var i interface{} = "hello"
    
    s := i.(string)
    fmt.Println(s)
    
    s, ok := i.(string)
    fmt.Println(s, ok)
    
    f, ok := i.(float64)
    fmt.Println(f, ok)
    
    f = i.(float64) // 报错(panic)
    fmt.Println(f)
    }
    

类型选择(type switch)

  • 类型选择语句用于判断某个 interface 变量中实际存储的变量类型

    package main
    
    import "fmt"
    
    func do(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Twice %v is %v\n", v, v*2)
    case string:
        fmt.Printf("%q is %v bytes long\n", v, len(v))
    default:
        fmt.Printf("I don't know about type %T!\n", v)
    }
    }
    
    func main() {
    do(21)
    do("hello")
    do(true)
    }
    

接口和类型的关系

一个类型可以实现多个接口

  • 接口间彼此独立,不知道对方的实现
  • Socket 结构的 Write() 方法实现了 io.Writer 以及 io.Closer 接口

    type Socket struct {
    }
    
    func (s *Socket) Write(p []byte) (n int, err error) {
      return 0, nil
    }
    
    func (s *Socket) Close() error {
      return nil
    }
    
    func usingWriter( writer io.Writer){
      writer.Write( nil ) // 使用io.Writer的代码, 并不知道Socket和io.Closer的存在
    }
    
    func usingCloser( closer io.Closer) {
      closer.Close() // 使用io.Closer, 并不知道Socket和io.Writer的存在
    }
    
    func main() {
      s := new(Socket) // 实例化Socket
      usingWriter(s)
      usingCloser(s)
    }
    

多个类型可以实现相同的接口

  • 接口的方法可以通过在类型中嵌入其他类型或者结构体来实现
  • 示例:

    • Service 接口定义了两个方法:一个是开启服务的方法(Start()),一个是输出日志的方法(Log())
    • 使用 GameService 结构体来实现 Service,GameService 自己的结构只能实现 Start() 方法,而 Service 接口中的 Log() 方法已经被一个能输出日志的日志器(Logger)实现了,无须再进行 GameService 封装,或者重新实现一遍
    • 选择将 Logger 嵌入到 GameService 能最大程度地避免代码冗余,简化代码结构

      type Service interface {
      Start()  // 开启服务
      Log(string)  // 日志输出
      }
      
      type Logger struct {
      }
      
      func (g *Logger) Log(l string) {
      }
      
      type GameService struct {
      Logger  // 嵌入日志器
      }
      
      func (g *GameService) Start() {
      }
      
      func main() {
      var s Service = new(GameService)
      s.Start()
      s.Log("hello")
      }
      

相关