Go 方法集合与选择receiver类型

Go 方法集合与选择receiver类型

文章目录

  • Go 方法集合与选择receiver类型
    • 一、receiver 参数类型对 Go 方法的影响
    • 二、选择 receiver 参数类型原则
      • 2.1 选择 receiver 参数类型的第一个原则
      • 2.2 选择 receiver 参数类型的第二个原则
    • 三、方法集合(Method Set)
      • 3.1 引入
      • 3.2 类型的方法集合
    • 四、选择 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)

这个例子中有方法 M1M2M1 方法是 receiver 参数类型为 T 的一类方法的代表,而 M2 方法则代表了 receiver 参数类型为 *T 的另一类。下面我们分别来看看不同的 receiver 参数类型对 M1M2 的影响。

首先,当 receiver 参数的类型为 T:当我们选择以 T 作为 receiver 参数类型时,M1 方法等价转换为 F1(t T)。我们知道,Go 函数的参数采用的是值拷贝传递,也就是说,F1 函数体中的 tT 类型实例的一个副本。这样,我们在 F1 函数的实现中对参数 t 做任何修改,都只会影响副本,而不会影响到原 T 类型实例。

据此我们可以得出结论:当我们的方法 M1 采用类型为 Treceiver 参数时,代表 T 类型实例的 receiver 参数以值传递方式传递到 M1 方法体中的,实际上是 T 类型实例的副本,M1 方法体中对副本的任何修改操作,都不会影响到原 T 类型实例。

第二,当 receiver 参数的类型为 *T:当我们选择以 *T 作为 receiver 参数类型时,M2 方法等价转换为 F2(t *T)。同上面分析,我们传递给 F2 函数的 tT 类型实例的地址,这样 F2 函数体中对参数 t 做的任何修改,都会反映到原 T 类型实例上。

据此我们也可以得出结论:当我们的方法 M2 采用类型为 *Treceiver 参数时,代表 *T 类型实例的 receiver 参数以值传递方式传递到 M2 方法体中的,实际上是 T 类型实例的地址,M2 方法体通过该地址可以对原 T 类型实例进行任何修改操作。

我们再通过一个更直观的例子,证明一下上面这个分析结果,看一下 Go 方法选择不同的 receiver 类型对原类型实例的影响:

package maintype T struct {a int
}func (t T) M1() {t.a = 10
}func (t *T) M2() {t.a = 11
}func main() {var t Tprintln(t.a) // 0t.M1()println(t.a) // 0p := &tp.M2()println(t.a) // 11
}

在这个示例中,我们为基类型 T 定义了两个方法 M1M2,其中 M1receiver 参数类型为 T,而 M2receiver 参数类型为 *TM1M2 方法体都通过 receiver 参数 tt 的字段 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 Tprintln(t1.a) // 0t1.M1()println(t1.a) // 0t1.M2()println(t1.a) // 11var t2 = &T{}println(t2.a) // 0t2.M1()println(t2.a) // 0t2.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,也就是与方法 M2receiver 参数类型 *T 不一致后,会自动将 t1.M2() 转换为 (&t1).M2()

同理,类型为 *T 的实例 t2,它不仅可以调用 receiver 参数类型为 *T 的方法 M2,还可以调用 receiver 参数类型为 T 的方法 M1,这同样是因为 Go 编译器在背后做了转换。也就是,Go 判断 t2 的类型为 *T,与方法 M1receiver 参数类型 T 不一致,就会自动将 t2.M1() 转换为 (*t2).M1()

通过这个实例,我们知道了这样一个结论:无论是 T 类型实例,还是 *T 类型实例,都既可以调用 receiverT 类型的方法,也可以调用 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 Tvar pt *Tvar i Interfacei = pti = t // cannot use t (type T) as type Interface in assignment: T does not implement Interface (M2 method has pointer receiver)
}

在这个例子中,我们定义了一个接口类型 Interface 以及一个自定义类型 TInterface 接口类型包含了两个方法 M1M2,代码中还定义了基类型为 T 的两个方法 M1M2,但它们的 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 intdumpMethodSet(n)dumpMethodSet(&n)var t TdumpMethodSet(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 定义了方法 M1M2,因此它的方法集合包含了 M1M2,也符合我们预期。但 *T 的方法集合中除了预期的 M3M4 之外,居然还包含了类型 T 的方法 M1M2

不过,这里程序的输出并没有错误。

这是因为,Go 语言规定,*T 类型的方法集合包含所有以 *Treceiver 参数类型的方法,以及所有以 Treceiver 参数类型的方法。这就是这个示例中为何 *T 类型的方法集合包含四个方法的原因。

这个时候,你是不是也找到了前面那个示例中为何 i = pt 没有报编译错误的原因了呢?我们同样可以使用 dumpMethodSet 工具函数,输出一下那个例子中 ptt 各自所属类型的方法集合:

type Interface interface {M1()M2()
}type T struct{}func (t T) M1()  {}
func (t *T) M2() {}func main() {var t Tvar pt *TdumpMethodSet(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 语言中的主要用途就是判断某个类型是否实现了某个接口。方法集合像“胶水”一样,将自定义类型与接口隐式地“粘结”在一起,

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/132297.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

公开IP属地信息如何保护用户的隐私?

公开IP属地信息通常涉及与用户或组织的隐私有关&#xff0c;因此在公开此类信息时需要非常小心&#xff0c;以避免侵犯他人的隐私权。以下是触碰底线的几种情况以及如何保护网络安全和用户隐私&#xff1a; 个人隐私保护&#xff1a; 公开IP属地信息可能泄露用户的物理位置&…

最新版一媒体7.3、星媒体、皮皮剪辑,视频MD ,安卓手机剪辑去重神器+搬运脚本+去视频重软件工具

最新版一媒体app安卓版介绍&#xff1a; 这是一款功能强大的视频搬运工具&#xff0c;内置海量视频编辑工具&#xff0c;支持一键智能化处理、混剪、搬运、还能快速解析和去水印等等&#xff0c;超多实用功能等着您来体验&#xff01; 老牌手机剪辑去重神器&#xff0c;用过的…

uniapp 编译到模拟器(mumu)

一开始我是用逍遥模拟器&#xff0c;但这个玩意突然不好使了&#xff0c;一直加载卡在这页面 1、下载 官网下载&#xff1a;mumu模拟器12 2、打开mumu多开器&#xff0c;在右上角adb查看端口号 3、打开mumu模拟器 4、打开HBuiderX 工具—设置—运行配置 5、配置电脑的系统…

各种各类好用热门API推荐

各种各类的好用API推荐&#xff0c;含免费次数~ 天气预报查询&#xff1a;查询全国以及全球多个城市的天气&#xff0c;包含15天天气预报查询。天气预警&#xff1a;可以获取指定城市当前生效中的各类天气预警&#xff0c;如寒潮蓝色预警信号&#xff0c;或一次性拉取全国所有…

了解web3,什么是web3

Web3是指下一代互联网&#xff0c;它基于区块链技术&#xff0c;将各种在线活动更加安全、透明和去中心化。Web3是一个广义的概念&#xff0c;它包括了很多方面&#xff0c;如数字货币、去中心化应用、智能合约等等。听不懂且大多数人听到这个东西&#xff0c;直觉感觉就像骗子…

centos 搭建 zookeeper 高可用集群

zookeeper-ha 主机名IP地址spark01192.168.171.101spark02192.168.171.102spark03192.168.171.103 1. 升级内核和软件 yum -y update2. 安装常用软件 yum -y install gcc gcc-c autoconf automake cmake make \zlib zlib-devel openssl openssl-devel pcre-devel \rsync op…

Python新手必读:容器类型使用的实用小贴士

更多资料获取 &#x1f4da; 个人网站&#xff1a;涛哥聊Python Python提供了多种容器类型&#xff0c;如列表&#xff08;List&#xff09;、元组&#xff08;Tuple&#xff09;、集合&#xff08;Set&#xff09;、字典&#xff08;Dictionary&#xff09;等&#xff0c;用于…

linux查看文件夹使用情况以及查看文件大小

查看文件夹的使用情况&#xff0c;包含已用和可用空间大小 df -h 文件夹路径然后再查看里面某一个文件夹占用大小 du -sh /data1、ls ls 命令是 Linux 中最常用的文件和目录列表命令之一。它可以显示文件的各种属性&#xff0c;包括文件大小。 ls -l <文件名>上述命令…

mybatis在springboot当中的使用

1.当使用Mybatis实现数据访问时&#xff0c;主要&#xff1a; - 编写数据访问的抽象方法 - 配置抽象方法对应的SQL语句 关于抽象方法&#xff1a; - 必须定义在某个接口中&#xff0c;这样的接口通常使用Mapper作为名称的后缀&#xff0c;例如AdminMapper - Mybatis框架底…

AI:57-基于机器学习的番茄叶部病害图像识别

🚀 本文选自专栏:AI领域专栏 从基础到实践,深入了解算法、案例和最新趋势。无论你是初学者还是经验丰富的数据科学家,通过案例和项目实践,掌握核心概念和实用技能。每篇案例都包含代码实例,详细讲解供大家学习。 📌📌📌在这个漫长的过程,中途遇到了不少问题,但是…

基于Pytorch框架的LSTM算法(一)——单维度单步滚动预测(2)

#项目说明&#xff1a; 说明&#xff1a;1time_steps滚动预测代码 y_norm scaler.fit_transform(y.reshape(-1, 1)) y_norm torch.FloatTensor(y_norm).view(-1)# 重新预测 window_size 12 future 12 L len(y)首先对模型进行训练&#xff1b; 然后选择所有数据的后wind…

汇编-字符串

字符串常量是用单引号或双引号括起来的一个字符序列 当以下面例子中的方式使用时&#xff0c;嵌入引号也是允许的&#xff1a; 正如字符常量以整数形式存放一样&#xff0c;字符串常量在内存中的存储形式为整数字节值的序列。例如&#xff0c; 字符串字面量“ABCD”包含四个字…

【代码】【5 二叉树】d3

关键字&#xff1a; 非叶子结点数、k层叶子结点数、层次遍历、找双亲结点、找度为1、叶子结点数

Eolink Apikit 版本更新:「数据字典」功能上线、支持 MongoDB 数据库操作、金融行业私有化协议、GitLab 生成 API 文档...

&#x1f389; 新增 搭建自定义接口协议架构&#xff0c;支持快速适配金融行业各类型私有协议的导入、编辑和展示。 数据字典功能上线&#xff0c;支持以数据字典的形式管理参数枚举值&#xff1b; 数据库连接支持 MongoDB 数据库操作&#xff1b; 基于 Apikit 类型导入 API…

如何设置没有采购申请不允许创建采购订单(TCODE:OMET)<转载>

原文链接 &#xff1a; https://mp.weixin.qq.com/s/0kcj9JWltlZoYhmzlwvT5g 在SAP/ERP项目实施中可能经常会遇到这样的业务需求&#xff0c;在系统中创建采购订单PO必须要有采购申请PR&#xff0c;否则不允许创建采购订单&#xff0c;通常这样业务需求一般通过采购订单增强去实…

每天一个注解之@RestControlleradvice

RestControlleradvice RestControllerAdvice 是一个Spring框架中的注解&#xff0c;用于处理全局异常&#xff0c;并将异常处理逻辑集中到一个类中&#xff0c;以减少代码重复性并提供一致的异常处理。通常&#xff0c;RestControllerAdvice 注解与ExceptionHandler 注解结合使…

携程AI布局:三重创新引领旅游行业智能化升级

2023年10月24日&#xff0c;携程全球合作伙伴峰会在新加坡召开&#xff0c;携程集团联合创始人、董事局主席梁建章做了名为《旅游业是独一无二的最好的行业》的演讲&#xff0c;梁建章在演讲中宣布了携程生成式 AI、内容榜单、ESG 低碳酒店标准三重创新的战略方向。这些创新将为…

206.反转链表

206.反转链表 力扣题目链接(opens new window) 题意&#xff1a;反转一个单链表。 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL 双双指针法&#xff1a; 创建三个节点 pre(反转时的第一个节点)、cur(当前指向需要反转的节点…

利用shp文件构建mask【MATLAB和ARCGIS】两种方法

1 ARCGIS &#xff08;推荐&#xff01;&#xff01;&#xff01;-速度很快&#xff09; 利用Polygon to Raster 注意&#xff1a;由于我们想要的mask有效值是1&#xff0c;在进行转换的时候&#xff0c;注意设置转换字段【Value field】 【Value field】通过编辑shp文件属性表…

注电考试科目、题量、分值、时间分配,题型及建议

注册电气工程师是国家承认的一种职业资格&#xff0c;具有较高的含金量。 主要体现在以下几个方面&#xff1a; 1.考试难度高&#xff1a;注册电气工程师考试是一项国家级考试&#xff0c;考试内容涵盖了该领域的多个方面&#xff0c;知识要求较高&#xff0c;考试难度较大。…