接口的约定

对于Go的具体类型来说,一个具体的类型可以准确的描述它所代表的值,并且展示出对类型本身的一些操作方式:就像数字类型的算术操作,切片类型的取下标、添加元素和范围获取操作。

与具体类型相对应的就是接口,一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合;它们只会表现出它们自己的方法。也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。

举例

字符串格式化一般使用两种方法:fmt.Sprintf/fmt.Printf
一个将格式化字符串作为返回值返回,另一个将格式化字符串输出到标准输入。

不必因为返回结果在使用方式上的一些浅显不同就必需把格式化这个最困难的过程复制一份。实际上,这两个函数都使用了另一个函数fmt.Fprintf来进行封装。fmt.Fprintf这个函数对它的计算结果会被怎么使用是完全不知道的。

  1. package fmt
  2. func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
  3. func Printf(format string, args ...interface{}) (int, error) {
  4. return Fprintf(os.Stdout, format, args...)
  5. }
  6. func Sprintf(format string, args ...interface{}) string {
  7. var buf bytes.Buffer
  8. Fprintf(&buf, format, args...)
  9. return buf.String()
  10. }

io.Writer也是一个接口类型,定义如下:

  1. package io
  2. // Writer is the interface that wraps the basic Write method.
  3. type Writer interface {
  4. // Write writes len(p) bytes from p to the underlying data stream.
  5. // It returns the number of bytes written from p (0 <= n <= len(p))
  6. // and any error encountered that caused the write to stop early.
  7. // Write must return a non-nil error if it returns n < len(p).
  8. // Write must not modify the slice data, even temporarily.
  9. //
  10. // Implementations must not retain p.
  11. Write(p []byte) (n int, err error)
  12. }

只需要传入的类型实现了Write函数,就能调用Writer接口,一个类型可以自由地被另一个满足相同接口的类型替换,满足里氏替换原则(LSP)。

里氏替换LSP:子类可以扩展父类的功能,但不能改变父类原有的功能。

实现接口的条件

Go的程序员经常会简要的把一个具体的类型描述成一个特定的接口类型。举个例子,*bytes.Buffer是io.ReadWriter。

表达一个类型属于某个接口只需要实现这个接口的所有方法:

  1. var w io.Writer
  2. w = os.Stdout // OK: *os.File has Write method
  3. w = new(bytes.Buffer) // OK: *bytes.Buffer has Write method
  4. w = time.Second // compile error: time.Duration lacks Write method
  5. var rwc io.ReadWriteCloser
  6. rwc = os.Stdout // OK: *os.File has Read, Write, Close methods
  7. rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method

即使具体类型有其它的方法,也只有接口类型暴露出来的方法会被调用到:

  1. os.Stdout.Write([]byte("hello")) // OK: *os.File has Write method
  2. os.Stdout.Close() // OK: *os.File has Close method
  3. var w io.Writer
  4. w = os.Stdout
  5. w.Write([]byte("hello")) // OK: io.Writer has Write method
  6. w.Close() // compile error: io.Writer lacks Close method

举例

一个具体的类型可能实现了很多不相关的接口。比如一个组织出售数字文化产品如音乐,电影和书籍。
程序中可能定义了下列的具体类型:

  1. Album
  2. Book
  3. Movie
  4. Magazine
  5. Podcast
  6. TVEpisode
  7. Track

我们可以把每个抽象的特点用接口来表示。

  1. // 一些特性对于所有的这些文化产品都是共通的,例如标题,创作日期和作者列表
  2. type Artifact interface {
  3. Title() string
  4. Creators() []string
  5. Created() time.Time
  6. }
  7. // 其它的一些特性只对特定类型的文化产品才有
  8. type Text interface {
  9. Pages() int
  10. Words() int
  11. PageSize() int
  12. }
  13. type Audio interface {
  14. Stream() (io.ReadCloser, error)
  15. RunningTime() time.Duration
  16. Format() string // e.g., "MP3", "WAV"
  17. }
  18. type Video interface {
  19. Stream() (io.ReadCloser, error)
  20. RunningTime() time.Duration
  21. Format() string // e.g., "MP4", "WMV"
  22. Resolution() (x, y int)
  23. }

每一个具体类型的组基于它们相同的行为可以表示成一个接口类型。不像基于类的语言,他们一个类实现的接口集合需要进行显式的定义,在Go语言中我们可以在需要的时候定义一个新的抽象或者特定特点的组,而不需要修改具体类型的定义。