接口
接口类型是对其他类型行为的概括与抽象。通过使用接口,我们可以写出更加灵活和通用的函数,这些函数不用绑定在一个特定类型的实现上。
很多面向对象的编程语言都有接口的概念,Go语言的接口的独特之处在于它是隐式实现的。换句话说,对于一个具体的类型,无需声明它实现了哪些接口,只要提供接口所必需的方法即可。这种设计让你无需改变已有的类型的实现,就可以为这些类型创建新的接口,对于那些不能修改包的类型,这一点特别有用。
声明接口并实现
声明一个Car
接口,并new
出三种车辆去实现这个接口,代码如下:
package main
import (
"fmt"
)
type Car interface {
run()
}
type BuckCar struct {
}
type AudiCar struct {
}
type BMWCar struct {
}
func (buck BuckCar) run() {
fmt.Println("I am BuckCar, I can run!")
}
func (audicar AudiCar) run() {
fmt.Println("I am AudiCar, I can run!")
}
func (bmwCar BMWCar) run() {
fmt.Println("I am BMWCar, I can run!")
}
func main() {
var car Car
car = new(BuckCar)
car.run()
car = new(AudiCar)
car.run()
car = new(BMWCar)
car.run()
}
接口即约定
接口是一种抽象类型,它并没有暴露所含数据的布局或者内部结构,当然也没有那些数据的基本操作,它所提供的仅仅是一些方法而已。
我们平时使用较多的fmt.Printf()
和fmt.Sprintf()
,前者负责把结果发到标准输出(标准输出其实是一个文件),后者把结果以string
类型返回。格式化是这两个函数中最复杂的部分,这两个函数的很类似,但是是有差异的,但是如果因为这一点差异就把这两个函数的格式化重新写一遍,那就太繁琐了!
恰好接口机制就可以解决这个问题。下图就是,Go SDK关于fmt.Printf()
和fmt.Sprintf()
,封装fmt.Fprintf()
的代码。
Fprintf
的前缀F
指文件,表示格式化的输出会写入第一个实参所指代的文件。对于Pringtf
,第一个实参就是os.Stdout
,它属于*os.File
类型。对于Sprintf
,尽管第一个实参不是文件,但它模拟了一个文件:&buf
就是一个指向内存缓冲区的指针,与文件类似,这个缓冲区也可以写入多个字节。
io.Writer
接口定义了Fprintf
和调用者之间的约定。一方面,这个约定要求调用者提供的具体类型(比如:*os.File
或者*byte.Buffer
)包含一个与其签名和行为一致的Writer
方法。另一方面,这个约定保证了Fprintf
能使用任何满足io.Writer
接口的参数。Fprintf
只需要能调用参数的Write
函数,无需假设它写入的是一个文件还是一段内存。因为fmt.Fprintf
仅依赖io.Writer
接口约定的方法,对参数的具体类型没有要求。
接口类型
一个接口类型定义了一套方法,如果一个具体类型要实现该接口,那么必须实现接口类型中的所有方法。
io.Writer
是一个广泛使用的接口,它负责所有可以写入字节的类型抽象,包括文件、内存缓冲区、网络连接、HTTP客户端、打包器(archiver)、散列器(hasher)等。io
包还定义了很多接口。Reader
就抽象了所有可以读取字节的类型,Closer
就抽象了所有可以关闭的类型,比如文件或者网络连接。
package io
type Reader interface{
Read(p []byte)(n int,err error)
}
type Closer interface{
Closer() error
}