https://studygolang.com/articles/1113

概述

在 Go 语言中,如果一个结构体和一个嵌入字段同时实现了相同的接口会发生什么呢?我们猜一下,可能有两个问题:

  • 编译器会因为我们同时有两个接口实现而报错吗?
  • 如果编译器接受这样的定义,那么当接口调用时编译器要怎么确定该使用哪个实现?

在写了一些测试代码并认真深入的读了一下标准之后,我发现了一些有意思的东西,而且觉得很有必要分享出来,那么让我们先从 Go 语言中的方法开始说起。

方法

Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。

下面定义一个结构体类型和该类型的一个方法:

type User struct {
Name string
Email string
} func (u User) Notify() error

首先我们定义了一个叫做 User 的结构体类型,然后定义了一个该类型的方法叫做 Notify,该方法的接受者是一个 User 类型的值。要调用 Notify 方法我们需要一个 User 类型的值或者指针:

// User 类型的值可以调用接受者是值的方法
damon := User{"AriesDevil", "ariesdevil@xxoo.com"}
damon.Notify() // User 类型的指针同样可以调用接受者是值的方法
alimon := &User{"A-limon", "alimon@ooxx.com"}
alimon.Notify()

在这个例子中当我们使用指针时,Go 调整和解引用指针使得调用可以被执行。注意,当接受者不是一个指针时,该方法操作对应接受者的值的副本(意思就是即使你使用了指针调用函数,但是函数的接受者是值类型,所以函数内部操作还是对副本的操作,而不是指针操作,参见:http://play.golang.org/p/DBhWU0p1Pv)。

我们可以修改 Notify 方法,让它的接受者使用指针类型:

func (u *User) Notify() error

再来一次之前的调用(注意:当接受者是指针时,即使用值类型调用那么函数内部也是对指针的操作,参见:http://play.golang.org/p/SYBb4xPfPh):

// User 类型的值可以调用接受者是指针的方法
damon := User{"AriesDevil", "ariesdevil@xxoo.com"}
damon.Notify() // User 类型的指针同样可以调用接受者是指针的方法
alimon := &User{"A-limon", "alimon@ooxx.com"}
alimon.Notify()

如果你不清楚到底什么时候该使用值,什么时候该使用指针作为接受者,你可以去看一下这篇介绍。这篇文章同时还包含了社区约定的接受者该如何命名。

接口

Go 语言中的接口很特别,而且提供了难以置信的一系列灵活性和抽象性。它们指定一个特定类型的值和指针表现为特定的方式。从语言角度看,接口是一种类型,它指定一个方法集,所有方法为接口类型就被认为是该接口。

下面定义一个接口:

type Notifier interface {
Notify() error
}

我们定义了一个叫做 Notifier 的接口并包含一个 Notify 方法。当一个接口只包含一个方法时,按照 Go 语言的约定命名该接口时添加 -er 后缀。这个约定很有用,特别是接口和方法具有相同名字和意义的时候。

我们可以在接口中定义尽可能多的方法,不过在 Go 语言标准库中,你很难找到一个接口包含两个以上的方法。

实现接口

当涉及到我们该怎么让我们的类型实现接口时,Go 语言是特别的一个。Go 语言不需要我们显式的实现类型的接口。如果一个接口里的所有方法都被我们的类型实现了,那么我们就说该类型实现了该接口。

让我们继续之前的例子,定义一个函数来接受任意一个实现了接口 Notifier 的类型的值或者指针:

func SendNotification(notify Notifier) error {
return notify.Notify()
}

SendNotification 函数调用 Notify 方法,这个方法被传入函数的一个值或者指针实现。这样一来一个函数就可以被用来执行任意一个实现了该接口的值或者指针的指定的行为。

用我们的 User 类型来实现该接口并且传入一个 User 类型的值来调用 SendNotification 方法:

func (u *User) Notify() error {
log.Printf("User: Sending User Email To %s<%s>\n",
u.Name,
u.Email)
return nil
} func main() {
user := User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
} SendNotification(user)
} // Output:
cannot use user (type User) as type Notifier in function argument:
User does not implement Notifier (Notify method has pointer receiver)

详细代码:http://play.golang.org/p/KG8-Qb7gqM

为什么编译器不考虑我们的值是实现该接口的类型?接口的调用规则是建立在这些方法的接受者和接口如何被调用的基础上。下面的是语言规范里定义的规则,这些规则用来说明是否我们一个类型的值或者指针实现了该接口:

  • 类型 *T 的可调用方法集包含接受者为 *T 或 T 的所有方法集

这条规则说的是如果我们用来调用特定接口方法的接口变量是一个指针类型,那么方法的接受者可以是值类型也可以是指针类型。显然我们的例子不符合该规则,因为我们传入 SendNotification 函数的接口变量是一个值类型。

  • 类型 T 的可调用方法集包含接受者为 T 的所有方法

这条规则说的是如果我们用来调用特定接口方法的接口变量是一个值类型,那么方法的接受者必须也是值类型该方法才可以被调用。显然我们的例子也不符合这条规则,因为我们 Notify 方法的接受者是一个指针类型。

语言规范里只有这两条规则,我通过这两条规则得出了符合我们例子的规则:

  • 类型 T 的可调用方法集不包含接受者为 *T 的方法

我们碰巧赶上了我推断出的这条规则,所以编译器会报错。Notify 方法使用指针类型作为接受者而我们却通过值类型来调用该方法。解决办法也很简单,我们只需要传入 User 值的地址到 SendNotification 函数就好了:

func main() {
user := &User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
} SendNotification(user)
} // Output:
User: Sending User Email To AriesDevil<ariesdevil@xxoo.com>

详细代码:http://play.golang.org/p/kEKzyTfLjA

嵌入类型

结构体类型可以包含匿名或者嵌入字段。也叫做嵌入一个类型。当我们嵌入一个类型到结构体中时,该类型的名字充当了嵌入字段的字段名。

下面定义一个新的类型然后把我们的 User 类型嵌入进去:

type Admin struct {
User
Level string
}

我们定义了一个新类型 Admin 然后把 User 类型嵌入进去,注意这个不叫继承而叫组合。 User 类型跟 Admin 类型没有关系。

我们来改变一下 main 函数,创建一个 Admin 类型的变量并把变量的地址传入 SendNotification 函数中:

func main() {
admin := &Admin{
User: User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
},
Level: "master",
} SendNotification(admin)
} // Output
User: Sending User Email To AriesDevil<ariesdevil@xxoo.com>

详细代码:http://play.golang.org/p/ivzzzk78TC

事实证明,我们可以 Admin 类型的一个指针来调用 SendNotification 函数。现在 Admin 类型也通过来自嵌入的 User 类型的方法提升实现了该接口。

如果 Admin 类型包含了 User 类型的字段和方法,那么它们在结构体中的关系是怎么样的呢?

当我们嵌入一个类型,这个类型的方法就变成了外部类型的方法,但是当它被调用时,方法的接受者是内部类型(嵌入类型),而非外部类型。– Effective Go

因此嵌入类型的名字充当着字段名,同时嵌入类型作为内部类型存在,我们可以使用下面的调用方法:

admin.User.Notify()

// Output
User: Sending User Email To AriesDevil<ariesdevil@xxoo.com>

详细代码:http://play.golang.org/p/0WL_5Q6mao

这儿我们通过类型名称来访问内部类型的字段和方法。然而,这些字段和方法也同样被提升到了外部类型:

admin.Notify()

// Output
User: Sending User Email To AriesDevil<ariesdevil@xxoo.com>

详细代码:http://play.golang.org/p/2snaaJojRo

所以通过外部类型来调用 Notify 方法,本质上是内部类型的方法。

下面是 Go 语言中内部类型方法集提升的规则:

给定一个结构体类型 S 和一个命名为 T 的类型,方法提升像下面规定的这样被包含在结构体方法集中:

  • 如果 S 包含一个匿名字段 T,S 和 *S 的方法集都包含接受者为 T 的方法提升。

这条规则说的是当我们嵌入一个类型,嵌入类型的接受者为值类型的方法将被提升,可以被外部类型的值和指针调用。

  • 对于 *S 类型的方法集包含接受者为 *T 的方法提升

这条规则说的是当我们嵌入一个类型,可以被外部类型的指针调用的方法集只有嵌入类型的接受者为指针类型的方法集,也就是说,当外部类型使用指针调用内部类型的方法时,只有接受者为指针类型的内部类型方法集将被提升。

  • 如果 S 包含一个匿名字段 *T,S 和 *S 的方法集都包含接受者为 T 或者 *T 的方法提升

这条规则说的是当我们嵌入一个类型的指针,嵌入类型的接受者为值类型或指针类型的方法将被提升,可以被外部类型的值或者指针调用。

这就是语言规范里方法提升中仅有的三条规则,我根据这个推导出一条规则:

  • 如果 S 包含一个匿名字段 T,S 的方法集不包含接受者为 *T 的方法提升。

这条规则说的是当我们嵌入一个类型,嵌入类型的接受者为指针的方法将不能被外部类型的值访问。这也是跟我们上面陈述的接口规则一致。

回答开头的问题

现在我们可以写程序来回答开头提出的两个问题了,首先我们让 Admin 类型实现 Notifier 接口:

func (a *Admin) Notify() error {
log.Printf("Admin: Sending Admin Email To %s<%s>\n",
a.Name,
a.Email) return nil
}

Admin 类型实现的接口显示一条 admin 方面的信息。当我们使用 Admin 类型的指针去调用函数 SendNotification 时,这将帮助我们确定到底是哪个接口实现被调用了。

现在创建一个 Admin 类型的值并把它的地址传入 SendNotification 函数,来看看发生了什么:

func main() {
admin := &Admin{
User: User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
},
Level: "master",
} SendNotification(admin)
} // Output
Admin: Sending Admin Email To AriesDevil<ariesdevil@xxoo.com>

详细代码:http://play.golang.org/p/JGhFaJnGpS

预料之中,Admin 类型的接口实现被 SendNotification 函数调用。现在我们用外部类型来调用 Notify 方法会发生什么呢:

admin.Notify()

// Output
Admin: Sending Admin Email To AriesDevil<ariesdevil@xxoo.com>

详细代码:http://play.golang.org/p/EGqK6DwBOi

我们得到了 Admin 类型的接口实现的输出。User 类型的接口实现不被提升到外部类型了。

现在我们有了足够的依据来回答问题了:

  • 编译器会因为我们同时有两个接口实现而报错吗?

不会,因为当我们使用嵌入类型时,类型名充当了字段名。嵌入类型作为结构体的内部类型包含了自己的字段和方法,且具有唯一的名字。所以我们可以有同一接口的内部实现和外部实现。

  • 如果编译器接受这样的定义,那么当接口调用时编译器要怎么确定该使用哪个实现?

如果外部类型包含了符合要求的接口实现,它将会被使用。否则,通过方法提升,任何内部类型的接口实现可以直接被外部类型使用。

总结

在 Go 语言中,方法,接口和嵌入类型一起工作方式是独一无二的。这些特性可以帮助我们像面向对象那样组织结构然后达到同样的目的,并且没有其它复杂的东西。用本文中谈到的语言特色,我们可以以极少的代码来构建抽象和可伸缩性的框架。

参考

Go 语言中的方法,接口和嵌入类型的更多相关文章

  1. 019_go语言中的方法

    代码演示 package main import "fmt" type rect struct { width, heigh int } func (r *rect) area() ...

  2. laravel迁移文件中字段方法对应的数据库类型

    /* *Blueprint类中的方法方法 <-> 数据库数据类型 * */ // 数字 increments();// int(10) unsigned primarykey auto_i ...

  3. 004_自己尝试go语言中的方法

    go语言可以给任意类型定义方法,我在学习过程中,一开始一头雾水,但是随着理解的深入,现在也大概知道了什么叫做方法 之前的一些例子其实讲的并不是特别生动,下面我用一个生动的例子演示一下 首先提出需求.我 ...

  4. Go语言中的方法和函数

    在C#或者Java里面我们都知道,一个Class是要包含成员变量和方法的,对于GO语言的Struct也一样,我们也可以给Struct定义一系列方法. 一.怎么定义一个方法? Go的方法是在函数前面加上 ...

  5. [Go] gocron源码阅读-go语言中的切片接口和类型综合

    // getCommands func getCommands() []cli.Command { command := cli.Command{ Name: "web", Usa ...

  6. go语言中的方法method

    package main; import "fmt" //重新定义一个类型 //为该INT类型扩展方法 type INT int; type A struct { name str ...

  7. ————————C语言中快速排序方法——————————————

    在对浮点型排序是一定要用三木运算符(三目运算符内容下去自己看),因为如果也是用整形那样的减法的时候如果是两个十分相近的数字 可能返回一个小数(自己一会去试试),冉冉他cmp返回值是int(试试别的)因 ...

  8. C语言中的系统时间结构体类型

    在C语言涉及中经常需要定时触发事件,涉及到获取系统时间,其结构体类型有多种.Unix/Linux系统下有以下几种时间结构: 1.time_t 类型:长整型,一般用来表示从1970-01-01 00:0 ...

  9. c语言中的结构体为值类型,当把一个结构体赋值给另一个结构体时,为值传递

    #include <stdio.h> int main() { struct person { int age; }; }; //值传递,将p1中所有成员变量的值赋值个p2中对应的成员变量 ...

随机推荐

  1. 20155210 Exp7 网络欺诈防范

    Exp7 网络欺诈防范 SET工具建立冒名网站 首先利用lsof -i:80或者netstat -tupln |grep 80查询80端口的使用情况(我的电脑80端口没有被占用,如果被占用,则用kil ...

  2. 20155307实验八 《网络对抗》 Web基础

    20155307实验八 <网络对抗> Web基础 实验过程 Web前端:HTML 使用netstat -aptn查看80端口是否被占用(上次实验设置为Apache使用80端口),如果被占用 ...

  3. TensorFlow训练MNIST数据集(3) —— 卷积神经网络

    前面两篇随笔实现的单层神经网络 和多层神经网络, 在MNIST测试集上的正确率分别约为90%和96%.在换用多层神经网络后,正确率已有很大的提升.这次将采用卷积神经网络继续进行测试. 1.模型基本结构 ...

  4. 记录一次Docker For Windows10镜像加速器配置

    1.访问https://www.daocloud.io 注册账号 2.访问资源->加速器,或者直接访问网址https://www.daocloud.io/mirror,页面中间有加速配置,例如我 ...

  5. Jenkins 构建运行java程序

    我们将在Jenkins建立执行一个简单的 HelloWorld 应用程序,构建和运行Java程序.打开网址:http://localhost:8080/jenkins 第1步- 转到Jenkins 仪 ...

  6. 教你如何自学UI设计

    一.常用的UI相关工具软件 PS Adobe Illustrator(AI) C4D AE Axure Sketch 墨刀 Principle Cutterman PxCook Zeplin 蓝湖 X ...

  7. ELK日志方案--使用Filebeat收集日志并输出到Kafka

    1,Filebeat简介 Filebeat是一个使用Go语言实现的轻量型日志采集器.在微服务体系中他与微服务部署在一起收集微服务产生的日志并推送到ELK. 在我们的架构设计中Kafka负责微服务和EL ...

  8. 高可用Kubernetes集群-13. 部署kubernetes-dashboard

    参考文档: Github介绍:https://github.com/kubernetes/dashboard Github yaml文件:https://github.com/kubernetes/d ...

  9. 理解以太坊的Layer 2扩容解决方案:状态通道(State Channels)、Plasma 和 Truebit

    -宾夕法尼亚州的尼科尔森大桥建设照片(图源).罗马人的工程原理扩展至新的应用 对于以太坊来说,2018年是专注底层架构之年.今年很多早期参与者会测试网络极限,并且重新关注以太坊的扩容技术. 以太坊仍然 ...

  10. 2013337朱荟潼 Linux第三章读书笔记——进程管理

    第三章 进程管理 总结 fork创造的子进程复制了父进程资源,包括内存及进程描述符的内容,资源的复制而不是指针的复制. vfork的行为更像一个线程(指没有自已独立的内存空间),更明显的是vfork的 ...