多态是指代码可以根据类型的具体实现采取不同行为的能力。如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值。例如标准库里io包里实现的流式处理接口。io包提供了一组构造的非常好的接口和函数,来让代码轻松支持流式数据处理。只要实现两个接口,就能利用整个io包背后的所有强大能力。
下面是一个示例程序
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func init(){
if len(os.Args)!=2{
fmt.Println("Usage: ./example2 <url>")
os.Exit(-1)
}
}
func main(){
r,err := http.Get(os.Args[1])
if err != nil{
fmt.Println(err)
return
}
io.Copy(os.Stdout,r.Body)
if err := r.Body.Close();err != nil{
fmt.Println(err)
}
}
调用了http包的Get函数。在与服务器成功通信后,http.Get函数会返回一个http.Response类型的指针。http.Response类型包含一个名为Body的字段,这个字段是一个io.ReadCloser接口类型的值。Body字段作为第二个参数传给io.Copy函数。io.Copy函数的第二个参数,接受一个io.Reader接口类型的值,这个值表示数据流入的源。Body字段实现了io.Reader接口,因此我们可以将Body字段传入io.Copy,使用Web服务器的返回内容作为源。
1. 实现
接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。
对接口值方法的调用会执行接口值里存储的用户定义的类型的值对应的方法。因为任何用户定义的类型都可以实现任何接口,所以对接口值方法的调用自然就是一种多态。在这个关系里,用户定义的类型通常叫做实体类型,原因是如果离开内部存储的用户定义的类型的值得实现,接口值没有具体的行为。
2. 方法集
方法集定义了接口的接受规则。下面是示例程序
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>\n",u.name,u.email)
}
func main(){
u:=user{"Bill","bill@email.com"}
sendNotification(u)
}
//接受一个实现了notifier接口的值并发送通知
func sendNotification(n notifier){
n.notify()
}
这个程序看起来没有问题,但却无法通过编译。编译器告诉我们user类型的值并没有实现这个接口。
要了解指针接收者来实现接口时为什么user类型的值无法实现该接口,需要先了解方法集。方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法时关联到值,还是关联到指针,还是两个都关联。
1)Go语言规范里定义的方法集的规则:
Values Methods Receivers
T (t T)
*T (t T)and(t *T)
T类型的值方法集只包含值接收者声明的方法。而指向T类型的指针的方法集既包含值接收者声明的方法,也包含指针接收者声明的方法。
2)从接收者类型的角度来看方法集:
Methods Values Values
(t T) T and T*
(t *T) *T
上述展示了同样的规则,只不过换成了接收者的视角。这个规则说,如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值接收者来实现一个接口,那么那个类型的值和指针都能实现对应的接口。
现在的问题是为什么会有这种限制?编译器并不是总能自动获得一个值的地址,如下所示
package main
import "fmt"
type duration int
func (d *duration) pretty() string {
return fmt.Sprintf("Duration:%d", *d)
}
func main() {
duration(42).pretty() //不能通过指针调用duration(42)的方法,不能获取duration(42)的地址
}
因为不是总能获取一个值得地址,所以值得方法集只包括了使用值接收者实现的方法
3. 多态
现在了解了接口和方法集背后的机制,最后来看一个展示接口的多态行为的例子
package main
import "fmt"
//定义了一个通知行为的接口
type notifier interface {
notify()
}
//user在程序里定义一个用户类型
type user struct{
name string
email string
}
//notify使用指针接收者实现了notifier接口
func (u *user) notify(){
fmt.Printf("Sending user email to %s<%s>\n",u.name,u.email)
}
//定义了程序里的管理员
type admin struct{
name string
email string
}
//notify使用指针接收者实现了notifier接口
func (a *admin) notify(){
fmt.Printf("Sending user email to %s<%s>\n",a.name,a.email)
}
func main(){
bill:=user{"Bill","bill@email.com"}
sendNotification(&bill)
lisa:=admin{"Lisa","lisa@email.com"}
sendNotification(&lisa)
}
func sendNotification(n notifier){
n.notify()
}
其中sendNotification是多态函数,这个函数接收一个实现了notifier接口的值作为参数。既然任意一个实体类型都能实现该接口,那么这个函数可以针对任意实体类型的值来执行notifier方法。因此,这个函数就能提供多态的行为。因为sendNotification接收notifier类型的接口值,所以这个函数可以同时执行user和admin实现的行为。
二、嵌入类型
Go语言允许用户扩展或者修改已有类型的行为。这个功能对代码复用很重要,在修改已有类型以符合新类型的时候也很重要。这个功能是通过嵌入类型完成的。嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。
通过嵌入类型,与内部类型相关的标识符会提升到外部类型上。这些被提升的标识符就像直接声明在外部类型里的标识符一样,也是外部类型的一部分。这样外部类型就组合了内部类型包含的所有属性,并且可以添加新的字段和方法。外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法。这就是扩展或者修改已有类型的方法。
下面是一个示例程序演示嵌入类型的基本方法
package main
import "fmt"
//在程序里定义一个用户类型
type user struct{
name string
email string
}
func (u *user)notify(){
fmt.Printf("Sending user email to %s<%s>\n",u.name,u.email)
}
type admin struct {
user //嵌入类型
level string
}
func main(){
ad:=admin{
user:user{
name:"john smith"
email:"john@yahoo.com"
},
level:"super",
}
//我们可以直接访问内部类型的方法
ad.user.notify()
//内部类型的方法被提升到外部类型
ad.notify()
}
要嵌入一个类型,只需要声明这个类型的名字就可以了。注意声明字段和嵌入类型在语法上的不同。通过内部类型的名字可以访问内部类型,如ad.user.notify()。不过,借助于内部类型的提升,notify方法也可以直接通过ad变量来访问,如ad.notify()。
由于内部类型的标识符提升到了外部类型,我们可以直接通过外部类型的值来访问内部类型的标识符。让我们修改一下这个例子,加入一个接口,如下所示
package main
import "fmt"
type notifier interface {
notify()
}
//在程序里定义一个用户类型
type user struct{
name string
email string
}
func (u *user)notify(){
fmt.Printf("Sending user email to %s<%s>\n",u.name,u.email)
}
type admin struct {
user //嵌入类型
level string
}
func main(){
ad:=admin{
user:user{
name:"john smith",
email:"john@yahoo.com",
},
level:"super",
}
//给admin用户发送一个通知,用于实现接口的内部类型的方法,被提升到外部类型
sendNotification(&ad)
}
func sendNotification(n notifier){
n.notify()
}
我们创建了一个名为ad的变量,其类型是外部类型admin。这个类型内部嵌入了user类型。之后将这个外部类型变量的地址传给sendNotification函数。编译器认为这个指针实现了notifier接口,并接受了这个值得传递。不过程序中admin类型并没有实现这个接口。由于内部类型的提升,内部类型实现的接口会自动提升到外部类型。这意味着由于内部类型的实现,外部类型也同样实现了这个接口。
如果外部类型并不需要使用内部类型的实现,而想要自己的一套实现,下面另一个示例程序解决了这个问题。
package main
import "fmt"
type notifier interface {
notify()
}
//在程序里定义一个用户类型
type user struct{
name string
email string
}
func (u *user)notify(){
fmt.Printf("Sending user email to %s<%s>\n",u.name,u.email)
}
type admin struct {
user //嵌入类型
level string
}
func (a *admin)notify(){
fmt.Printf("Sending admin email to %s<%s>\n",a.name,a.email)
}
func main(){
ad:=admin{
user:user{
name:"john smith",
email:"john@yahoo.com",
},
level:"super",
}
//给admin用户发送一个通知,接口的嵌入内部类型实现并没有被提升到外部类型
sendNotification(&ad)
//我们可以直接访问内部类型的放法
ad.user.notify()
//内部类型的方法没有被提升
ad.notify()
}
func sendNotification(n notifier){
n.notify()
}
这表明,如果外部类型实现了notify方法,内部类型的实现就不会被提升。不过内部类型的值一直存在,因此还可以通过直接访问内部类型的值,来调用没有被提升的内部类型实现的方法。