《Go语言学习笔记》读书笔记(5)接口

2 分钟阅读

接口

接口代表一种调用契约,是多个方法声明的集合。接口最常见的使用场景,是对包外提供访问,或预留扩展空间。 Go接口的实现机制很简洁,只要目标类型方法集内包含接口声明的全部方法,就被视为实现了该接口,无须做显式声明。当然,目标类型可实现多个接口。 接口:

  • 不能有字段
  • 不能定义自己的方法
  • 只能声明方法,不能实现
  • 可嵌入其他接口类型

接口通常以er作为名称后缀,方法名是声明组成部分,但参数名可不同或省略。

type tester interface {
  test()
  string() string
}

type data struct {}

func (*data) test() {}
func (data) string() string() {return "";}

func main() {
  var d data

  /// var t tester = d  ///< 错误

  var t tester = &d
  t.test()
  println(t.string())
}

如果接口没有任何声明方法声明,那么就是一个空接口, 他的用途类似面向对象的根类型Object, 可被赋值为任何类型的对象。 接口变量默认值是nil。如果实现接口的类型支持,可做相等运算。

func main() {
  var t1, t2 interface{}
  println(t1 == nil, t1 == t2)

  t1, t2 = 100, 100
  println(t1 == t2)
  t1, t2 = map[string]int{}, map[string]int{}
  println(t1 == t2)
}
/// 输出:
/// true true
/// true
/// panic: runtime error: comparing uncomparable type map[string]int

可以像匿名字段一样,嵌入其他接口。目标类型方法集中必须拥有包含嵌入接口方法在内的全部方法才算实现了该接口。 前提是,不能有同名方法, 不能嵌入自身或循环嵌入,那会导致递归错误。

type stringer interface {
  string() string
}

type tester interface {
  stringer  ///< 嵌入接口
  test()
}

func (*data) test() {}
func (data) string() string{
  return ""
}

func main() {
  var d data
  var t tester = &d
  t.test()
  println(t.string())
}

超集接口变量可隐式转换为子集,反过来不行。

func pp(a stringer) {
  println(a.string())
}

func main() {
  var d data
  var t tester = &d
  pp(t) ///< 隐式转换为自己接口
  var s stringer = t  ///< 超集转换为子集
  println(s.string())
}

支持匿名接口类型,可直接用于变量定义,或作为结构字段类型。

type data struct{}
func (data) string() string {
  return ""
}
type node struct {
  data interface {  ///< 匿名接口类型
    string() string
  }
}

func main() {
  var t interface { ///< 定义匿名接口变量
    string() string
  } = data{}

  n := node{
    data: t,
  }
  println(n.data.string())
}

执行机制

接口执行一个名为itab的结构存储运行期所需的相关类型信息。

type iface struct {
  tab *itab ///< 类型信息
  data unsafe.Pointer ///< 实际对象指针
}
type itab struct {
  inter *interfacetype  ///< 接口类型
  _type *_type  ///< 实际对象类型
  fun [1]uintptr  ///< 实际对象方法地址
}

相关类型信息里保存了接口和实际对象的元数据。同时itab还用fun数组(不定长结构)保存了实际方法地址,从而实现在运行期对目标方法的动态调用。 除此之外,接口还有一个重要特征:将对象赋值给接口变量时,会复制该对象。我们甚至无法修改结构存储的复制品,因为它也是unaddressable的。

func main() {
  d := data{100}
  vat t interface{} = d
  p := &t.(data)  ///< 错误
  t.(data).x = 200  ///< 错误
}

即便将其复制出来,用本地变量修改后,依然无法对iface.data赋值。解决方法就是将对象指针赋值给接口,那么接口内存存储的就是指针的复制品。 只有当接口变量内部的两个指针(itab, data)都为nil时, 接口才等于nil.

类型转换

类型推断可将接口变量还原为原始类型,或用来判断是否实现了某个更具体地接口类型。

type data int
func (d data) String() string() {
  return fmt.Sprintf("data:%d", d)
}

func main() {
  var d data = 15
  var x interface{} = d

  if n, ok := x.(fmt.Stringer); ok {  ///< 转换为更具体地接口类型
    fmt.Println(n)
  }

  if d2, ok := x.(data); ok { ///< 转换回原始类型
    fmt.Println(d2)
  }

  e := x.(error)  ///< 错误: main.data is not error
  fmt.Println(e)
}

使用ok-idiom模式,即便转换失败也不会引发panic。还可用switch语句在多种类型间做出推断匹配,这样空接口就有更多发挥空间。

func main() {
  var x interface{} = func(x int) string {
    return fmt.Sprintf("d:%d", x)
  }
  switch v := x(type) {
    case nil:
      println("nil")
    case *int:
      println(*v)
    case func(int) string:
      println(v(100))
    case fmt.Stringer:
      fmt.Println(v)
    default:
      println("unknown")
  }
}
/// 输出:
/// d: 100

提示: type switch不支持fallthrought

技巧

让编译器检查,确保类型实现了指定接口

type x int
func init() { ///< 包初始函数
  var _ fmt.Stringer = x(0)
}

定义函数类型,让相同签名地函数自动实现某个接口

type FuncString func() string

func (f FuncString) String() string {
  return f()
}

func main() {
  var t fmt.Stringer = FuncString(func() string {
  return "hello, world!"
})
fmt. Println(t)
}