接口约定
目前为止,我们看到的类型都是具体的类型。一个具体的类型可以准确的描述它所代表的值,并且展示出对类型本身的一些操作方式:就像数字类型的算术操作,切片类型的取下标、添加元素和范围获取操作。具体的类型还可以通过它的内置方法提供额外的行为操作。总的来说,当你拿到一个具体的类型时你就知道它的本身是什么和你可以用它来做什么。
在 Go 语言中还存在着另外一种类型:接口类型。接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合;它们只会表现出它们自己的方法。也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。
在本书中,我们一直使用两个相似的函数来进行字符串的格式化:fmt.Printf,它会把结果写到标准输出,和 fmt.Sprintf,它会把结果以字符串的形式返回。得益于使用接口,我们不必可悲的因为返回结果在使用方式上的一些浅显不同就必需把格式化这个最困难的过程复制一份。实际上,这两个函数都使用了另一个函数 fmt.Fprintf 来进行封装。fmt.Fprintf 这个函数对它的计算结果会被怎么使用是完全不知道的。
package fmt
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
func Printf(format string, args ...interface{}) (int, error) {
return Fprintf(os.Stdout, format, args...)
}
func Sprintf(format string, args ...interface{}) string {
var buf bytes.Buffer
Fprintf(&buf, format, args...)
return buf.String()
}
Fprintf 的前缀 F 表示文件(File)也表明格式化输出结果应该被写入第一个参数提供的文件中。在 Printf 函数中的第一个参数 os.Stdout 是 *os.File 类型;在 Sprintf 函数中的第一个参数 &buf 是一个指向可以写入字节的内存缓冲区,然而它并不是一个文件类型尽管它在某种意义上和文件类型相似。
即使 Fprintf 函数中的第一个参数也不是一个文件类型。它是 io.Writer 类型,这是一个接口类型定义如下:
package io
// Writer is the interface that wraps the basic Write method.
type Writer interface {
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
Write(p []byte) (n int, err error)
}
io.Writer 类型定义了函数 Fprintf 和这个函数调用者之间的约定。一方面这个约定需要调用者提供具体类型的值就像 *os.File 和 *bytes.Buffer ,这些类型都有一个特定签名和行为的 Write 的函数。另一方面这个约定保证了 Fprintf 接受任何满足 io.Writer 接口的值都可以工作。Fprintf 函数可能没有假定写入的是一个文件或是一段内存,而是写入一个可以调用 Write 函数的值。
因为 fmt.Fprintf 函数没有对具体操作的值做任何假设,而是仅仅通过 io.Writer 接口的约定来保证行为,所以第一个参数可以安全地传入一个只需要满足 io.Writer 接口的任意具体类型的值。一个类型可以自由地被另一个满足相同接口的类型替换,被称作可替换性(LSP里氏替换)。这是一个面向对象的特征。
让我们通过一个新的类型来进行校验,下面 *ByteCounter
类型里的 Write 方法,仅仅在丢弃写向它的字节前统计它们的长度。(在这个 += 赋值语句中,让 len(p) 的类型和 *c
的类型匹配的转换是必须的。)
Unresolved include directive in modules/ROOT/pages/ch7/ch7-01.adoc - include::example$/ch7/bytecounter/main.go[]
因为 *ByteCounter
满足 io.Writer 的约定,我们可以把它传入 Fprintf 函数中;Fprintf 函数执行字符串格式化的过程不会去关注 ByteCounter 正确的累加结果的长度。
Unresolved include directive in modules/ROOT/pages/ch7/ch7-01.adoc - include::example$/ch7/bytecounter/main.go[]
除了 io.Writer 这个接口类型,还有另一个对 fmt 包很重要的接口类型。 Fprintf 和 Fprintln 函数向类型提供了一种控制它们值输出的途径。在2.5节中,我们为 Celsius 类型提供了一个 String 方法以便于可以打印成这样"100°C" ,在6.5节中我们给 *IntSet
添加一个 String 方法,这样集合可以用传统的符号来进行表示就像 {1 2 3}
。给一个类型定义 String 方法,可以让它满足最广泛使用之一的接口类型 fmt.Stringer :
package fmt
// The String method is used to print values passed
// as an operand to any format that accepts a string
// or to an unformatted printer such as Print.
type Stringer interface {
String() string
}
我们会在 7.10 节解释 fmt 包怎么发现哪些值是满足这个接口类型的。
练习 7.1: 使用来自 ByteCounter 的思路,实现一个针对单词和行数的计数器。你会发现 bufio.ScanWords 非常的有用。
练习 7.2: 写一个带有如下函数签名的函数 CountingWriter,传入一个 io.Writer 接口类型,返回一个把原来的 Writer 封装在里面的新的 Writer 类型和一个表示新的写入字节数的 int64 类型指针。
func CountingWriter(w io.Writer) (io.Writer, *int64)
练习 7.3: 为在 gopl.io/ch4/treesort(§4.4)中的 *tree
类型实现一个 String 方法去展示 tree 类型的值序列。