多态是指代码可以根据类型的具体实现采取不同行为的能力。
如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值。
标准库里有很好的例子,如io包里实现的流式处理接口。
io包提供了一组构造得非常好得接口和函数,来让代码轻松支持流式数据处理。
只要实现两个接口,就能利用整个io包背后得所有强大能力。
1.标准库
先来看一个示例程序,这个程序实现了流行程序curl得功能。
示例1:
//这个示例程序展示如何使用io.Reader和io.Writer接口 //写一个简单版本得curl程序 package main import ( "fmt" "io" "net/http" "os" ) //init在main函数之前调用 func init() { if len(os.Args) != 2 { fmt.Println("Usage: ./example<url>") os.Exit(-1) } } //main是应用程序得入口 func main() { //从web服务器得到响应 r, err := http.Get(os.Args[1]) if err != nil { fmt.Println(err) range } //从Body复制到Stdout io.Copy(os.Stdout, r.Body) if err := r.Body.Close(); err != nil { fmt.Println(err) } }
上述代码展示了接口的能力以及在标准库里的应用。
只用了几行代码我们就通过两个函数以及配套的接口,完成了curl程序。
下面是部分代码的解释:
r, err := http.Get(os.Args[1])
调用了http包的get函数。在与服务器成功通信后,http.Get函数会返回一个http.Response类型的指针。
http.Response类型包含一个名为Body的字段,这个字段是一个io.ReadCloser接口类型的值。
io.Copy(os.Stdout, r.Body)
Body字段作为第二个参数传给io.Copy函数。io.Copy函数的第二个参数接受一个io.Reader接口类型的值,这个值表示数据流入的源。
Body字段实现了io.Reader接口,因此我们可以将Body字段传入io.Copy,使用Web服务器的返回内容作为源。
io.Copy的第一个参数是复制到目标,这个参数必须是一个实现了io.Writer接口的值。
对于这个目标,我们传入os包里的一个特殊值Stdout。这个接口值表示标准输出设备,并且已经实现了io.Writer接口。
当我们将Body和Stdout这两个值传给io.Copy函数后,这个函数会把服务器的数据分成小段,
源源不断地传给终端窗口,直到最后一个片段读取并写入到终端,io.Copy函数才返回。
io.Copy函数可以以这种工作流的方式处理很多标准库里已有的类型。
示例2:
//这个示例程序展示bytes.Buffer也可以 //用io.Copy函数 package main import ( "bytes" "fmt" "io" "os" ) //main是应用程序的入口 func main() { var b bytes.Buffer //将字符串写入Buffer b.Write([]byte("Hello")) //将字符串拼接到Buffer fmt.Fprintf(&b, "World!") //将Buffer的内容写到Stdout io.Copy(os.Stdout, &b) }
这个程序使用接口来拼接字符串,并将数据以流的方式输出到标准输出设备。
var b bytes.Buffer
创建一个bytes包里的Buffer类型的变量b,用于缓冲数据。
b.Write([]byte("Hello"))
使用Write方法将字符串Hello写入到缓冲区b。
fmt.Fprintf(&b, "World!")
调用fmt包里的Fprintf函数,将第二个字符串追加到缓冲区b里。
io.Copy(os.Stdout, &b)
将字符写道终端。
fmt.Fprintf函数接受一个io.Writer类型的接口值作为其第一个参数。
由于bytes.Buffer类型的指针实现了io.Writer接口,所以可以将缓存b传入fmt.Fprintf函数,并执行追加操作。
由于bytes.Buffer类型的指针也实现了io.Reader接口,io.Copy函数可以用于在终端窗口显式缓冲区b的内容。
2.实现
接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。
如果用户定义的类型实现了某个接口声明的一组方法,那么这个用户定义的类型的值可以赋给这个接口类型的值。
这个赋值会把用户定义的类型的值存入接口类型的值。
对接口值方法的调用会执行接口值里存储的用户定义的类型的值对应的方法。
因为任何用户定义的类型都可以实现任何接口,所以对接口值方法的调用自然就是一种多态。
在这个关系里,用户定义的类型通常叫做实体类型,原因是如果离开了内部存储的用户定义的类型的值的实现,接口值并没有具体的行为。
并不是所有值都完全等同,用户定义的类型的值或者指针要满足接口的实现,需要遵守一些规则。
上图展示了在user类型值赋值后接口变量的值的内部布局。
接口值是一个两个字长度的数据结构,第一个字包含一个指向内部表的指针。
这个内部表叫做iTable,包含了所存储的值的类型信息。
iTable包含了已存储的值类型信息以及与这个值相关联的一组方法。
第二个字是一个指向所存储值得指针。将类型信息和指针组合在一起,就将这两个值组成了一种特殊得关系。
上图展示了一个指针赋值给接口之后发生的变化。
在这种情况里,类型信息会存储一个指向保存的类型的指针,而接口值第二个字依旧保存指向实体值的指针。
3.方法集
package main import ( "fmt" ) //notifier是一个接口定义 //通知类行为的接口 type notifier interface { notify() } //user在程序里定义了一个用户类型 type user struct { name string email string } //notify是使用指针接收者实现的方法 func (u *user) notify() { fmt.Printf("Sending user email to %s<%s> ", u.name, u.email) } func main() { //创建一个user类型的值,并发送通知 u := user{"Bill", "bill@email.com"} sendNotification(&u) } //声明了一个函数,这个函数接收一个notifier接口类型的值 //之后使用这个接口值来调用notify方法。 //任何一个实现了notifier接口的值都可以传入sendNotification函数。 func sendNotification(n notifier) { n.notify() //Sending user email to Bill<bill@email.com> }
方法集定义了一组关联到给定类型的值或指针的方法。
定义方法时使用的接收者的类型决定了这个方法是关联到值还是关联到指针,还是两者都关联。
T类型的值的方法集只包含值接收者声明的方法。
而指向T类型的指针的方法集既包含值接收者声明的方法,也包含指针接收者声明的方法。
如果从接受者的视角来看。
如果使用执政接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。
如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。