第7章 接口
接口类型是对其它类型行为的抽象和概括.接口类型不会和特定的实现细节绑定在一起,这种抽象的方式能让我们的函数更加的灵活和更具有适应能力
Go语言的接口比较特殊,因为它是满足隐式实现的。也就是说,我们无需给具体类型定义所有满足足的接口类型,只需要让类型拥有一些简单必要的方法。这样我们新建一个接口类型,满足具体类型,并且我们不需要更改这些类型的定义。当我们使用的类型来自于不受我们控制的包时,这种机制比较有用
7.1 接口是合约
截至目前,我们都学的时具体类型,一个具体类型除了代表一个值之外,还展示出对类型本身的一些操作方式,如像数字类型的算术操作等,总而言之,就是当我们看到一个具体类型的时候,我们就知道它能干什么以及它是什么
接口类型是一种抽象类型,它不会暴露它所代表的对象的内部值的结构和该对象支持的基础操作,它只会表现出它自己的方法,也就是说,当你拥有一个接口值,你知道可以通过它的方法来做什么,而不知道它是什么
我们来看两个函数Printf和Sprintf
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
func Sprintf(format string, a ...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类型,它是一个接口,定义如下:
type Writer interface{
Write(p []byte) (n int,err error)
}
接口类型定义了其所在函数和函数调用者之间的约定:调用者需要提供具体类型,如*os.File和 *byets.Buffer,并且这些具体类型都必须有一个特定签名和行为的Write函数;约定保证Fprintf接受任何满足io.Writer接口的值都可以工作
Fprintf函数可能没有假定写入的是一个文件或者是一段内存,而是写入一个可以调用Write函数的值
fmt.Fprintf函数没有对具体操作的值做任何假设,而仅仅是通过io.Writer接口的约定来保证行为,所以第一个参数可以安全的传入一个只需要满足io.Writer接口的任意具体类型的值。一个类型可以自由的被另一个满足相同接口的类型替换,被称作可替换性(LSP里氏替换)。这是一个面向对象的特征
我们来校验一下,下面的Write方法是统计字节的长度
type ByteCounter int
func (c *ByteCounter) Write(p []byte)(int,error){
*c += ByteCounter(len(p))
return len(p),nil
}
因为*ByteCounter满足io.Writer的约定,我们可以把它传入Fprintf函数中,Fprintf函数将执行字符串格式化的过程不会去关注ByteCounter正确的累加结果的长度
var c ByteCounter
c.Write([]byte("hello"))
fmt.Println(c)
c = 0
var name = "Dolly"
fmt.Fprintf(&c,"hello,%s",name)
fmt.Println(c)
除了io.Writer这个接口类型,还有另一个对fmt包很重要的接口类型。Fprintf和Fprintln函数像类型提供了一种控制他们输出的途径。在2.5节,我们为Celsius类型提供了一个String方法以便可以打印这样的”100℃“,在6.5节中,我们给*InSet添加了一个String方法,这样的集合可以用传统的符号来进行表示,就像“{1 2 3}”,给一个定义string的方法,让它满足最广泛使用之一的接口类型fmt.Stringer
package fmt
type Stringer interface {
String() string
}
我们会在7.10解释fmt包怎么发现哪些值是满足这个接口类型的