https://www.jianshu.com/p/d87c69ac6ce7
概述
在 Go 语言中,如果一个结构体和一个嵌入字段同时实现了相同的接口会发生什么呢?我们猜一下,可能有两个问题:
- 编译器会因为我们同时有两个接口实现而报错吗?
- 如果编译器接受这样的定义,那么当接口调用时编译器要怎么确定该使用哪个实现?
实现接口
当涉及到我们该怎么让我们的类型实现接口时,Go 语言是特别的一个。Go 语言不需要我们显式的实现类型的接口。如果一个接口里的所有方法都被我们的类型实现了,那么我们就说该类型实现了该接口。
package main
import (
"log"
)
type User struct {
Name string
Email string
}
func (u *User) Notify() error {
log.Printf("User: Sending User Email To %s<%s>\n",
u.Name,
u.Email)
return nil
}
type Notifier interface {
Notify() error
}
func SendNotification(notify Notifier) error {
return notify.Notify()
}
func main() {
user := User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
}
SendNotification(user)
}
prog.go:34: cannot use user (type User) as type Notifier in argument to SendNotification: User does not implement Notifier (Notify method has pointer receiver)
为什么编译器不考虑我们的值是实现该接口的类型?接口的调用规则是建立在这些方法的接受者和接口如何被调用的基础上。下面的是语言规范里定义的规则,这些规则用来说明是否我们一个类型的值或者指针实现了该接口:
- 类型 *T 的可调用方法集包含接受者为 *T 或 T 的所有方法集
这条规则说的是如果我们用来调用特定接口方法的接口变量是一个指针类型,那么方法的接受者可以是值类型也可以是指针类型。显然我们的例子不符合该规则,因为我们传入 SendNotification 函数的接口变量是一个值类型。 - 类型 T 的可调用方法集包含接受者为 T 的所有方法
这条规则说的是如果我们用来调用特定接口方法的接口变量是一个值类型,那么方法的接受者必须也是值类型该方法才可以被调用。显然我们的例子也不符合这条规则,因为我们 Notify 方法的接受者是一个指针类型。
语言规范里只有这两条规则,我通过这两条规则得出了符合我们例子的规则: - 类型 T 的可调用方法集不包含接受者为 *T 的方法
我们碰巧赶上了我推断出的这条规则,所以编译器会报错。Notify 方法使用指针类型作为接受者而我们却通过值类型来调用该方法。解决办法也很简单,我们只需要传入 User 值的地址到 SendNotification 函数就好了:
package main
import (
"log"
)
type User struct {
Name string
Email string
}
func (u *User) Notify() error {
log.Printf("User: Sending User Email To %s<%s>\n",
u.Name,
u.Email)
return nil
}
type Notifier interface {
Notify() error
}
func SendNotification(notify Notifier) error {
return notify.Notify()
}
func main() {
user := &User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
}
SendNotification(user)
}
嵌入类型
结构体类型可以包含匿名或者嵌入字段。也叫做嵌入一个类型。当我们嵌入一个类型到结构体中时,该类型的名字充当了嵌入字段的字段名。
package main
import (
"log"
)
type User struct {
Name string
Email string
}
type Admin struct {
User
Level string
}
func (u *User) Notify() error {
log.Printf("User: Sending User Email To %s<%s>\n",
u.Name,
u.Email)
return nil
}
type Notifier interface {
Notify() error
}
func SendNotification(notify Notifier) error {
return notify.Notify()
}
func main() {
admin := &Admin{
User: User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
},
Level: "super",
}
SendNotification(admin)
}
详细代码:http://play.golang.org/p/ivzzzk78TC
事实证明,我们可以 Admin 类型的一个指针来调用 SendNotification 函数。现在 Admin 类型也通过来自嵌入的 User 类型的方法提升实现了该接口。
如果 Admin 类型包含了 User 类型的字段和方法,那么它们在结构体中的关系是怎么样的呢?
当我们嵌入一个类型,这个类型的方法就变成了外部类型的方法,但是当它被调用时,方法的接受者是内部类型(嵌入类型),而非外部类型。— Effective Go
因此嵌入类型的名字充当着字段名,同时嵌入类型作为内部类型存在,我们可以使用下面的调用方法:
admin.User.Notify()
// OutputUser: Sending User Email To AriesDevil<ariesdevil@xxoo.com>
这儿我们通过类型名称来访问内部类型的字段和方法。然而,这些字段和方法也同样被提升到了外部类型:
admin.Notify()
// OutputUser: Sending User Email To AriesDevil<ariesdevil@xxoo.com>
所以通过外部类型来调用 Notify 方法,本质上是内部类型的方法。
下面是 Go 语言中内部类型方法集提升的规则:
给定一个结构体类型 S 和一个命名为 T 的类型,方法提升像下面规定的这样被包含在结构体方法集中:
- 如果 S 包含一个匿名字段 T,S 和 *S 的方法集都包含接受者为 T 的方法提升。
这条规则说的是当我们嵌入一个类型,嵌入类型的接受者为值类型的方法将被提升,可以被外部类型的值和指针调用。
- 对于 *S 类型的方法集包含接受者为 *T 的方法提升
这条规则说的是当我们嵌入一个类型,可以被外部类型的指针调用的方法集只有嵌入类型的接受者为指针类型的方法集,也就是说,当外部类型使用指针调用内部类型的方法时,只有接受者为指针类型的内部类型方法集将被提升。
- 如果 S 包含一个匿名字段 *T,S 和 *S 的方法集都包含接受者为 T 或者 *T 的方法提升
这条规则说的是当我们嵌入一个类型的指针,嵌入类型的接受者为值类型或指针类型的方法将被提升,可以被外部类型的值或者指针调用。
这就是语言规范里方法提升中仅有的三条规则,我根据这个推导出一条规则:
如果 S 包含一个匿名字段 T,S 的方法集不包含接受者为 *T 的方法提升。
这条规则说的是当我们嵌入一个类型,嵌入类型的接受者为指针的方法将不能被外部类型的值访问。这也是跟我们上面陈述的接口规则一致。
回答开头的问题
现在我们可以写程序来回答开头提出的两个问题了,首先我们让 Admin 类型实现 Notifier 接口:
Admin 类型实现的接口显示一条 admin 方面的信息。当我们使用 Admin 类型的指针去调用函数 SendNotification 时,这将帮助我们确定到底是哪个接口实现被调用了。
现在创建一个 Admin 类型的值并把它的地址传入 SendNotification 函数,来看看发生了什么:
package main
import (
"log"
)
type User struct {
Name string
Email string
}
type Admin struct {
User
Level string
}
func (u *User) Notify() error {
log.Printf("User: Sending User Email To %s<%s>\n",
u.Name,
u.Email)
return nil
}
//func (a *Admin) Notify() error {
// log.Printf("Admin: Sending Admin Email To %s<%s>\n",
// a.Name,
// a.Email)
//
// return nil
//}
type Notifier interface {
Notify() error
}
func SendNotification(notify Notifier) error {
return notify.Notify()
}
func main() {
admin := &Admin{}
admin.Notify()
admin.User.Notify()
SendNotification(admin)
}
-
编译器会因为我们同时有两个接口实现而报错吗?
不会,因为当我们使用嵌入类型时,类型名充当了字段名。嵌入类型作为结构体的内部类型包含了自己的字段和方法,且具有唯一的名字。所以我们可以有同一接口的内部实现和外部实现。 -
如果编译器接受这样的定义,那么当接口调用时编译器要怎么确定该使用哪个实现?
如果外部类型包含了符合要求的接口实现,它将会被使用。否则,通过方法提升,任何内部类型的接口实现可以直接被外部类型使用。 -
总结
在 Go 语言中,方法,接口和嵌入类型一起工作方式是独一无二的。这些特性可以帮助我们像面向对象那样组织结构然后达到同样的目的,并且没有其它复杂的东西。用本文中谈到的语言特色,我们可以以极少的代码来构建抽象和可伸缩性的框架。