接口类型

复习日志(1)

* 本页面主要介绍Go语言接口类型的相关内容。

接口(interface)是Go语言中是一种特别的类型(没办法像其他类型一样被实例化),一种抽象的类型

一、接口定义

接口(interface)定义了一个对象的行为规范,只定义规范(方法定义)不实现,由具体的对象来实现规范的细节。

  //声明接口
  type Pet interface {
    SetName(name string)
    Name() string
    Category() string
  }

通过关键字type和interface,我们可以声明出接口类型。接口类型的零值是nil,可以和nil进行判等操作。接口类型声明中的这些方法所代表的就是该接口的方法集合。一个接口的方法集合就是它的全部特征

因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。但这是有风险的:如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic:

  var x interface{} = []int{1, 2, 3}
  fmt.Println(x == x) // panic: comparing uncomparable type []int

所以,要么慎用,要么就是在进行比较之前,要明确接口值的动态类型到底是不是可比较的。

二、接口内嵌

io.Writer类型是用得最广泛的接口之一,它提供了所有类型的写入bytes的抽象。

  package io
  type Reader interface {
      Read(p []byte) (n int, err error)
  }
  type Closer interface {
      Close() error
  }

基于此,我们可以通过组合已有接口定义新的接口类型

  type ReadWriter interface {
      Reader
      Writer
  }
  type ReadWriteCloser interface {
      Reader
      Writer
      Closer
  }

这种方式有点儿像结构内嵌,用这种方式以一个简写命名一个接口,而不用声明它所有的方法。这就称之为接口内嵌

Go 语言团队鼓励我们声明体量较小的接口,并建议我们通过这种接口间的组合来扩展程序、增加程序的灵活性。因为小接口可以更加专注地表达某一种能力或某一类特征,同时也更容易被组合在一起。

三、接口的实现

对于任何数据类型,只要它的方法集合中完全包含了一个接口的全部特征(即全部的方法),那么它就一定是这个接口的实现类型。这是一种无侵入式的接口实现方式。这种方式叫“鸭子类型”

If it walks like a duck and quacks like a duck then it is a duck.

如果它走起路来像鸭子,叫声也像鸭子,那么它就是鸭子。

鸭子类型这一名字出自James Whitcomb Riley在鸭子测试中提出的上述表述。鸭子类型(Duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。即:对象的类型不再由继承等方式决定,而由实际运行时所表现出的具体行为来决定

综上,一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。

  package main

  import "fmt"

  type Pet interface {
    SetName(name string)
    Name() string
    Category() string
  }

  type Dog struct {
    name string // 名字。
  }

  func (dog *Dog) SetName(name string) {
    dog.name = name
  }

  func (dog Dog) Name() string {
    return dog.name
  }

  func (dog Dog) Category() string {
    return "dog"
  }

  func main() {
    // 示例1。
    dog := Dog{"little pig"}
    _, ok := interface{}(dog).(Pet)
    fmt.Printf("Dog implements interface Pet: %v\n", ok)
    _, ok = interface{}(&dog).(Pet)
    fmt.Printf("*Dog implements interface Pet: %v\n", ok)
    fmt.Println()

    // 示例2。
    var pet Pet = &dog
    fmt.Printf("This pet is a %s, the name is %q.\n",
      pet.Category(), pet.Name())
  }

上面这个代码示例中,虽然结构体类型Dog不是Pet接口的实现类型,但它的指针类型*Dog却是这个的实现类型。Dog类型本身的方法集合中只包含了 2 个方法,也就是所有的值方法。而它的指针类型*Dog方法集合却包含了 3 个方法。即*Dog拥有Dog类型附带的所有值方法和指针方法。又由于这 3 个方法恰恰分别是Pet接口中某个方法的实现,所以*Dog类型就成为了Pet接口的实现类型。

实现了接口有什么用呢?最大的用处是:接口类型变量能够存储所有实现了该接口的实例。比如我再声明一个Cat变量实现了接口Pet,那么pet变量也可以存储&Cat。

四、类型和接口的关系
  • 一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现
  • 不同的类型还可以实现同一接口
  • 并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现
五、空接口

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。空接口类型的变量可以存储任意类型的变量

空接口一般应用在以下场合中:

  • 空接口作为函数的参数:可以接收任意类型的函数参数
  •   // 空接口作为函数参数
      func show(a interface{}) {
          fmt.Printf("type:%T value:%v\n", a, a)
      }
  • 空接口作为map的值:使用空接口实现可以保存任意值的字典
  •   // 空接口作为map值
      var studentInfo = make(map[string]interface{})
      studentInfo["name"] = "李白"
      studentInfo["age"] = 18
      studentInfo["married"] = false
      fmt.Println(studentInfo)

既然空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢? 这个时候我们就需要用到类型断言了!

六、接口变量的结构

我们知道,接口变量在以下两种情况下的值是真正的nil(我们把由字面量nil表示的值叫做无类型的nil,也就是真正的nil!):

  • 只声明,不做初始化的时候
  • 直接把字面量nil赋给它

第一种情况很好理解,那么第二种情况要怎么理解呢?这个时候,就需要了解下接口变量的值是怎么存储的了。

对于接口变量来说,是由动态值(实际值)动态类型(实际类型)两部分组成的。当我们给一个接口变量赋值的时候,该变量的动态类型会与它的动态值一起被存储在一个专用的数据结构中。 比如上面的例子中,pet类型变量的动态值是&dog的结果值,动态类型是*Dog。而相对于动态类型来讲,pet的静态类型则永远不变,一直都是Pet,不会随着赋给它的动态值而变化。

我们再通过一个小例子感受一下接口值的这一特征:

  var w io.Writer //零值nil,真正的nil
  w = os.Stdout //动态类型为*os.File,动态值是os.Stdout的拷贝
  w = new(bytes.Buffer) //动态类型为*bytes.Buffer,动态值是一个指向新分配的缓冲区的指针
  w = nil //恢复为真正的nil

上面的4个语句,变量w得到了3个不同的值。分别是注释中所展示的,结构变化见下图。

接口值的数据变化
接口值的数据变化1~2
接口值的数据变化
接口值的数据变化3~4

了解了接口变量值的存储结构之后,我们再看下面的例子:

  var dog1 *Dog
  fmt.Println("The first dog is nil. [wrap1]")
  dog2 := dog1
  fmt.Println("The second dog is nil. [wrap1]")
  var pet Pet = dog2
  if pet == nil {
    fmt.Println("The pet is nil. [wrap1]")
  } else {
    fmt.Println("The pet is not nil. [wrap1]")
  }

虽然dog1是“真正的nil”,但将dog1赋值给dog2的时候,dog2就不是“真正的nil”了,它的动态类型是*Dog,只是值是nil而已。这回能明白了吧!

关于接口,我们就总结这么多,你Get了吗?


* 本页内容参考以下数据源:

  • 《Go程序设计语言》
  • 《Go语言核心36讲》 14 接口类型的合理运用
  • https://stackoverflow.com/questions/4205130/what-is-duck-typing
  • https://blog.csdn.net/u014454539/article/details/84864472

凯冰科技 · 代码改变世界,技术改变生活
Next Page→