Go 方法集合与选择receiver类型
Go 方法集合与选择receiver类型
一、receiver 参数类型对 Go 方法的影响
要想为 receiver 参数选出合理的类型,我们先要了解不同的 receiver 参数类型会对 Go 方法产生怎样的影响。其实,Go 方法实质上是以方法的 receiver 参数作为第一个参数的普通函数。
对于函数参数类型对函数的影响,我们是很熟悉的。那么我们能不能将方法等价转换为对应的函数,再通过分析 receiver 参数类型对函数的影响,从而间接得出它对 Go 方法的影响呢?
基于这个思路。我们直接来看下面例子中的两个 Go 方法,以及它们等价转换后的函数:
func (t T) M1() <=> F1(t T)
func (t *T) M2() <=> F2(t *T)
这个例子中有方法 M1 和 M2。M1 方法是 receiver 参数类型为 T 的一类方法的代表,而 M2 方法则代表了 receiver 参数类型为 *T 的另一类。下面我们分别来看看不同的 receiver 参数类型对 M1 和 M2 的影响。
首先,当 receiver 参数的类型为 T 时:当我们选择以 T 作为 receiver 参数类型时,M1 方法等价转换为 F1(t T)。我们知道,Go 函数的参数采用的是值拷贝传递,也就是说,F1 函数体中的 t 是 T 类型实例的一个副本。这样,我们在 F1 函数的实现中对参数 t 做任何修改,都只会影响副本,而不会影响到原 T 类型实例。
据此我们可以得出结论:当我们的方法 M1 采用类型为 T 的 receiver 参数时,代表 T 类型实例的 receiver 参数以值传递方式传递到 M1 方法体中的,实际上是 T 类型实例的副本,M1 方法体中对副本的任何修改操作,都不会影响到原 T 类型实例。
第二,当 receiver 参数的类型为 *T 时:当我们选择以 *T 作为 receiver 参数类型时,M2 方法等价转换为 F2(t *T)。同上面分析,我们传递给 F2 函数的 t 是 T 类型实例的地址,这样 F2 函数体中对参数 t 做的任何修改,都会反映到原 T 类型实例上。
据此我们也可以得出结论:当我们的方法 M2 采用类型为 *T 的 receiver 参数时,代表 *T 类型实例的 receiver 参数以值传递方式传递到 M2 方法体中的,实际上是 T 类型实例的地址,M2 方法体通过该地址可以对原 T 类型实例进行任何修改操作。
我们再通过一个更直观的例子,证明一下上面这个分析结果,看一下 Go 方法选择不同的 receiver 类型对原类型实例的影响:
package main
type T struct {
a int
}
func (t T) M1() {
t.a = 10
}
func (t *T) M2() {
t.a = 11
}
func main() {
var t T
println(t.a) // 0
t.M1()
println(t.a) // 0
p := &t
p.M2()
println(t.a) // 11
}
在这个示例中,我们为基类型 T 定义了两个方法 M1 和 M2,其中 M1 的 receiver 参数类型为 T,而 M2 的 receiver 参数类型为 *T。M1 和 M2 方法体都通过 receiver 参数 t 对 t 的字段 a 进行了修改。
但运行这个示例程序后,我们看到,方法 M1 由于使用了 T 作为 receiver 参数类型,它在方法体中修改的仅仅是 T 类型实例 t 的副本,原实例并没有受到影响。因此 M1 调用后,输出 t.a 的值仍为 0。
而方法 M2 呢,由于使用了 *T 作为 receiver 参数类型,它在方法体中通过 t 修改的是实例本身,因此 M2 调用后,t.a 的值变为了 11,这些输出结果与我们前面的分析是一致的。
二、选择 receiver 参数类型原则
2.1 选择 receiver 参数类型的第一个原则
基于上面的影响分析,我们可以得到选择 receiver 参数类型的第一个原则:如果 Go 方法要把对 receiver 参数代表的类型实例的修改,反映到原类型实例上,那么我们应该选择 *T 作为 receiver 参数的类型。
可能会有个疑问:如果我们选择了 *T 作为 Go 方法 receiver 参数的类型,那么我们是不是只能通过 *T 类型变量调用该方法,而不能通过 T 类型变量调用了呢?我们改造上面例子看一下:
type T struct {
a int
}
func (t T) M1() {
t.a = 10
}
func (t *T) M2() {
t.a = 11
}
func main() {
var t1 T
println(t1.a) // 0
t1.M1()
println(t1.a) // 0
t1.M2()
println(t1.a) // 11
var t2 = &T{}
println(t2.a) // 0
t2.M1()
println(t2.a) // 0
t2.M2()
println(t2.a) // 11
}
我们先来看看类型为 T 的实例 t1。我们看到它不仅可以调用 receiver 参数类型为 T 的方法 M1,它还可以直接调用 receiver 参数类型为 *T 的方法 M2,并且调用完 M2 方法后,t1.a 的值被修改为 11 了。
其实,T 类型的实例 t1 之所以可以调用 receiver 参数类型为 *T 的方法 M2,都是 Go 编译器在背后自动进行转换的结果。或者说,t1.M2() 这种用法是 Go 提供的“语法糖”:Go 判断 t1 的类型为 T,也就是与方法 M2 的 receiver 参数类型 *T 不一致后,会自动将 t1.M2() 转换为 (&t1).M2()。
同理,类型为 *T 的实例 t2,它不仅可以调用 receiver 参数类型为 *T 的方法 M2,还可以调用 receiver 参数类型为 T 的方法 M1,这同样是因为 Go 编译器在背后做了转换。也就是,Go 判断 t2 的类型为 *T,与方法 M1 的 receiver 参数类型 T 不一致,就会自动将 t2.M1() 转换为 (*t2).M1()。
通过这个实例,我们知道了这样一个结论:无论是 T 类型实例,还是 *T 类型实例,都既可以调用 receiver 为 T 类型的方法,也可以调用 receiver 为 *T 类型的方法。这样,我们在为方法选择 receiver 参数的类型的时候,就不需要担心这个方法不能被与 receiver 参数类型不一致的类型实例调用了。
2.2 选择 receiver 参数类型的第二个原则
前面我们第一个原则说的是,当我们要在方法中对 receiver 参数代表的类型实例进行修改,那我们要为 receiver 参数选择 *T 类型,但是如果我们不需要在方法中对类型实例进行修改呢?这个时候我们是为 receiver 参数选择 T 类型还是 *T 类型呢?
这也得分情况。一般情况下,我们通常会为 receiver 参数选择 T 类型,因为这样可以缩窄外部修改类型实例内部状态的“接触面”,也就是尽量少暴露可以修改类型内部状态的方法。
不过也有一个例外需要你特别注意。考虑到 Go 方法调用时,receiver 参数是以值拷贝的形式传入方法中的。那么,如果 receiver 参数类型的 size 较大,以值拷贝形式传入就会导致较大的性能开销,这时我们选择 *T 作为 receiver 类型可能更好些。
以上这些可以作为我们选择 receiver 参数类型的第二个原则。
三、方法集合(Method Set)
3.1 引入
我们先通过一个示例,直观了解一下为什么要有方法集合,它主要用来解决什么问题:
type Interface interface {
M1()
M2()
}
type T struct{}
func (t T) M1() {}
func (t *T) M2() {}
func main() {
var t T
var pt *T
var i Interface
i = pt
i = t // cannot use t (type T) as type Interface in assignment: T does not implement Interface (M2 method has pointer receiver)
}
在这个例子中,我们定义了一个接口类型 Interface 以及一个自定义类型 T。Interface 接口类型包含了两个方法 M1 和 M2,代码中还定义了基类型为 T 的两个方法 M1 和 M2,但它们的 receiver 参数类型不同,一个为 T,另一个为 *T。在 main 函数中,我们分别将 T 类型实例 t 和 *T 类型实例 pt 赋值给 Interface 类型变量 i。
运行一下这个示例程序,我们在 i = t 这一行会得到 Go 编译器的错误提示,Go 编译器提示我们:T 没有实现 Interface 类型方法列表中的 M2,因此类型 T 的实例 t 不能赋值给 Interface 变量。
可是,为什么呢?为什么 *T 类型的 pt 可以被正常赋值给 Interface 类型变量 i,而 T 类型的 t 就不行呢?如果说 T 类型是因为只实现了 M1 方法,未实现 M2 方法而不满足 Interface 类型的要求,那么 *T 类型也只是实现了 M2 方法,并没有实现 M1 方法啊?
有些事情并不是表面看起来这个样子的。了解方法集合后,这个问题就迎刃而解了。同时,方法集合也是用来判断一个类型是否实现了某接口类型的唯一手段,可以说,“方法集合决定了接口实现”。
3.2 类型的方法集合
Go 中任何一个类型都有属于自己的方法集合,或者说方法集合是 Go 类型的一个“属性”。但不是所有类型都有自巴基斯坦的方法呀,比如 int 类型就没有。所以,对于没有定义方法的 Go 类型,我们称其拥有空方法集合。
接口类型相对特殊,它只会列出代表接口的方法列表,不会具体定义某个方法,它的方法集合就是它的方法列表中的所有方法,我们可以一目了然地看到。
为了方便查看一个非接口类型的方法集合,这里提供了一个函数 dumpMethodSet,用于输出一个非接口类型的方法集合:
func dumpMethodSet(i interface{}) {
dynTyp := reflect.TypeOf(i)
if dynTyp == nil {
fmt.Printf("there is no dynamic type\n")
return
}
n := dynTyp.NumMethod()
if n == 0 {
fmt.Printf("%s's method set is empty!\n", dynTyp)
return
}
fmt.Printf("%s's method set:\n", dynTyp)
for j := 0; j < n; j++ {
fmt.Println("-", dynTyp.Method(j).Name)
}
fmt.Printf("\n")
}
下面我们利用这个函数,试着输出一下 Go 原生类型以及自定义类型的方法集合,看下面代码:
type T struct{}
func (T) M1() {}
func (T) M2() {}
func (*T) M3() {}
func (*T) M4() {}
func main() {
var n int
dumpMethodSet(n)
dumpMethodSet(&n)
var t T
dumpMethodSet(t)
dumpMethodSet(&t)
}
运行这段代码,我们得到如下结果:
int's method set is empty!
*int's method set is empty!
main.T's method set:
- M1
- M2
*main.T's method set:
- M1
- M2
- M3
- M4
我们看到以 int、*int 为代表的 Go 原生类型由于没有定义方法,所以它们的方法集合都是空的。自定义类型 T 定义了方法 M1 和 M2,因此它的方法集合包含了 M1 和 M2,也符合我们预期。但 *T 的方法集合中除了预期的 M3 和 M4 之外,居然还包含了类型 T 的方法 M1 和 M2!
不过,这里程序的输出并没有错误。
这是因为,Go 语言规定,*T 类型的方法集合包含所有以 *T 为 receiver 参数类型的方法,以及所有以 T 为 receiver 参数类型的方法。这就是这个示例中为何 *T 类型的方法集合包含四个方法的原因。
这个时候,你是不是也找到了前面那个示例中为何 i = pt 没有报编译错误的原因了呢?我们同样可以使用 dumpMethodSet 工具函数,输出一下那个例子中 pt 与 t 各自所属类型的方法集合:
type Interface interface {
M1()
M2()
}
type T struct{}
func (t T) M1() {}
func (t *T) M2() {}
func main() {
var t T
var pt *T
dumpMethodSet(t)
dumpMethodSet(pt)
}
运行上述代码,我们得到如下结果:
main.T's method set:
- M1
*main.T's method set:
- M1
- M2
通过这个输出结果,我们可以一目了然地看到 T、*T 各自的方法集合。
我们看到,T 类型的方法集合中只包含 M1,没有 Interface 类型方法集合中的 M2 方法,这就是 Go 编译器认为变量 t 不能赋值给 Interface 类型变量的原因
在输出的结果中,我们还看到 *T 类型的方法集合除了包含它自身定义的 M2 方法外,还包含了 T 类型定义的 M1 方法,*T 的方法集合与 Interface 接口类型的方法集合是一样的,因此 pt 可以被赋值给 Interface 接口类型的变量 i。
到这里,我们已经知道了所谓的方法集合决定接口实现的含义就是:如果某类型 T 的方法集合与某接口类型的方法集合相同,或者类型 T 的方法集合是接口类型 I 方法集合的超集,那么我们就说这个类型 T 实现了接口 I。或者说,方法集合这个概念在 Go 语言中的主要用途,就是用来判断某个类型是否实现了某个接口。
四、选择 receiver 参数类型的第三个原则
理解了方法集合后,我们再理解第三个原则的内容就不难了。这个原则的选择依据就是 T 类型是否需要实现某个接口,也就是是否存在将 T 类型的变量赋值给某接口类型变量的情况。
理解了方法集合后,我们再理解第三个原则的内容就不难了。这个原则的选择依据就是 T 类型是否需要实现某个接口,也就是是否存在将 T 类型的变量赋值给某接口类型变量的情况。
如果 T 类型需要实现某个接口,那我们就要使用 T 作为 receiver 参数的类型,来满足接口类型方法集合中的所有方法。
如果 T 不需要实现某一接口,但 *T 需要实现该接口,那么根据方法集合概念,*T 的方法集合是包含 T 的方法集合的,这样我们在确定 Go 方法的 receiver 的类型时,参考原则一和原则二就可以了。
如果说前面的两个原则更多聚焦于类型内部,从单个方法的实现层面考虑,那么这第三个原则则是更多从全局的设计层面考虑,聚焦于这个类型与接口类型间的耦合关系。
五、小结
在实际进行 Go 方法设计时,我们首先应该考虑的是原则三,即 T 类型是否要实现某一接口。如果 T 类型需要实现某一接口的全部方法,那么我们就需要使用 T 作为 receiver 参数的类型来满足接口类型方法集合中的所有方法。
如果 T 类型不需要实现某一接口,那么我们就可以参考原则一和原则二来为 receiver 参数选择类型了。也就是,如果 Go 方法要把对 receiver 参数所代表的类型实例的修改反映到原类型实例上,那么我们应该选择 *T 作为 receiver 参数的类型。否则通常我们会为 receiver 参数选择 T 类型,这样可以减少外部修改类型实例内部状态的“渠道”。除非 receiver 参数类型的 size 较大,考虑到传值的较大性能开销,选择 *T 作为 receiver 类型可能更适合。
方法集合在 Go 语言中的主要用途就是判断某个类型是否实现了某个接口。方法集合像“胶水”一样,将自定义类型与接口隐式地“粘结”在一起,
Go 方法集合与选择receiver类型的更多相关文章
- Python爬虫防封杀方法集合
Python爬虫防封杀方法集合 mrlevo520 2016.09.01 14:20* 阅读 2263喜欢 38 Python 2.7 IDE Pycharm 5.0.3 前言 ...
- 个人永久性免费-Excel催化剂功能第104波-批量选择多种类型的图形对象
在Excel的日常操作过程中,选择绝对是一个高频的操作,之前开发过一些快速选择单元格区域的辅助功能,除了单元格区域,Excel强大之处在于,类似PhotoShop那般可以存放多种图形,并且有图层先后顺 ...
- CSS选择符类型
一.标签选择符:针对某一类标签,可以以标签作为选择符 <style type="text/css"> p{color:#F00; font-size:36px;} &l ...
- 八大排序方法汇总(选择排序,插入排序-简单插入排序、shell排序,交换排序-冒泡排序、快速排序、堆排序,归并排序,计数排序)
2013-08-22 14:55:33 八大排序方法汇总(选择排序-简单选择排序.堆排序,插入排序-简单插入排序.shell排序,交换排序-冒泡排序.快速排序,归并排序,计数排序). 插入排序还可以和 ...
- python3 selenium 随机选择同一类型下的某一个元素
使用场景: 如上图所示,有时候,我们测试的时候,不会每个方向都选择一遍,也不能每次都选择一个方向,这个时候就需要每次运行用例的时候,随机选择一个方向来测试 使用方法: random.randint() ...
- Go 语言中的方法,接口和嵌入类型
https://studygolang.com/articles/1113 概述 在 Go 语言中,如果一个结构体和一个嵌入字段同时实现了相同的接口会发生什么呢?我们猜一下,可能有两个问题: 编译器会 ...
- class字节码结构(四)(方法集合的结构)
<Java虚拟机原理图解>1.5. class文件中的方法表集合--method方法在class文件中是怎样组织的 一个类有多个方法,所以方法肯定是一个集合. 目标是: 1,了解方法在字节 ...
- 怎么进入bios设置界面,电脑如何进入BIOS进行设置,怎么进入BIOS的方法集合
怎么进入bios设置界面,电脑如何进入BIOS进行设置,怎么进入BIOS的方法集合 开机出现电脑商家图标时,按住F10键进入BIOS界面.进入BIOS界面一般都是开机后按<del,Esc,F1, ...
- Spring MVC--------处理方法返回值的可选类型
对于Spring MVC处理方法支持支持一系列的返回方式: (1)ModelAndView (2)Model (3)ModelMap (4)Map (5)View (6)String (7)Void ...
- 通过反射 往泛型Integer的集合里添加String 类型的数据 Day25
package com.sxt.method1; import java.lang.reflect.Method; /* * 需求:通过反射 往泛型Integer的集合里添加String 类型的数据 ...
随机推荐
- Django基本数据库操作
Django基本数据库操作 @ 目录 Django基本数据库操作 内容一:基本数据库配置 内容二:ORM基本操作 内容一:基本数据库配置 Django是一个流行的Python Web框架,它可以 ...
- 国产化之x64平台安装银河麒麟操作系统
背景 某个项目需要实现基础软件全部国产化,其中操作系统指定银河麒麟v4,CPU使用飞腾处理器.飞腾处理器是ARMv8架构的,在之前的文章中介绍了使用QEMU模拟ARMv8架构安装银河麒麟操作系统的方式 ...
- Docker安装及镜像加速器配置
Centos7安装 卸载旧版本(如果安装过旧版本的话) yum remove docker docker-common docker-selinux docker-engine 安装Docker依赖环 ...
- 我真的想知道,AI编译器中的IR是什么?
随着深度学习的不断发展,AI 模型结构在快速演化,底层计算硬件技术更是层出不穷,对于广大开发者来说不仅要考虑如何在复杂多变的场景下有效的将算力发挥出来,还要应对 AI 框架的持续迭代. AI 编译器就 ...
- 从0开发属于自己的nestjs框架的mini 版 —— ioc篇
如今,nodejs的框架也是层出不穷,偏向向底层的有 express.koa. Fastify,偏向于上层有阿里的 Egg.thinkjs .还有国外的 nestjs. 在这里我更喜欢 nestjs, ...
- Django常用配置
创建Django项目(命令行) 创建项目:打开终端,使用命令:django-admin startproject [项目名称]即可创建.比如:django-admin startproject fir ...
- [minio]挂载minio到本地
前言 将minio的bucket挂载到本地文件系统 环境 客户端系统版本:centos 7 MinIO节点IP:192.168.0.20 s3fs方式步骤 安装s3fs客户端(可能需要先安装epel- ...
- 何时使用MongoDB而不是MySql
什么是 MySQL 和 MongoDB MySQL 和 MongoDB 是两个可用于存储和管理数据的数据库管理系统.MySQL 是一个关系数据库系统,以结构化表格格式存储数据.相比之下,MongoDB ...
- Flutter 学习笔记(01)__从 0 开始创建一个 flutter 项目
最近发现有不少的公司已经跳出 uniapp 的坑坑,开始使用 flutter 开发app了,为了让自己不失业,赶紧卷起来!此篇文章教你从 0 基础开发一个 简单页面,文章篇幅较长,建议收藏!也可以直接 ...
- 解密Linux中的通用块层:加速存储系统,提升系统性能
通用块层 通用块层是Linux中的一个重要组件,用于管理不同块设备的统一接口,减少不同块设备的差异带来的影响.它位于文件系统和磁盘驱动之间,类似于Java中的适配器模式,让我们无需关注底层实现,只需提 ...