Go基础18-理解方法的本质以选择正确的receiver类型
Go语言虽然不支持经典的面向对象语法元素,比如类、对象、继承等,但Go语言也有方法。和函数相比,Go语言中的方法在声明形式上仅仅多了一个参数,Go称之为receiver参数。receiver参数是方法与类型之间的纽带。
Go方法的一般声明形式如下:
func (receiver T/*T) MethodName(参数列表) (返回值列表) {// 方法体
}
上面方法声明中的T称为receiver的基类型。通过receiver,上述方法被绑定到类型T上。换句话说,上述方法是类型T的一个方法,我们可以通过类型T或*T的实例调用该方法,如下面的伪代码所示:
var t T
t.MethodName(参数列表)
var pt *T = &t
pt.MethodName(参数列表)
Go方法具有如下特点。
1)方法名的首字母是否大写决定了该方法是不是导出方法。
2)方法定义要与类型定义放在同一个包内。由此我们可以推出:不能为原生类型(如int、float64、map等)添加方法,只能为自定义类型定义方法(示例代码如下)。
// 错误的做法
func (i int) String() string { // 编译器错误:cannot define new methods on non- local type intreturn fmt.Sprintf("%d", i)
}
// 正确的做法
type MyInt int
func (i MyInt) String() string {return fmt.Sprintf("%d", int(i))
}
同理,可以推出:不能横跨Go包为其他包内的自定义类型定义方法。
3)每个方法只能有一个receiver参数,不支持多receiver参数列表或变长receiver参数。一个方法只能绑定一个基类型,Go语言不支持同时绑定多个类型的方法。
4)receiver参数的基类型本身不能是指针类型或接口类型,下面的示例展示了这点:
type MyInt *int
func (r MyInt) String() string { // 编译器错误:invalid receiver type MyInt (MyInt is a pointer type)return fmt.Sprintf("%d", *(*int)(r))
}
type MyReader io.Reader
func (r MyReader) Read(p []byte) (int, error) { // 编译器错误:invalid receiver type MyReader (MyReader is an interface type)return r.Read(p)
}
和其他主流编程语言相比,Go语言从函数到方法仅多了一个receiver,这大大降低了Gopher学习方法的门槛。即便如此,Gopher在把握方法本质及选择receiver的类型时仍存有困惑,本条就针对这些困惑进行重点说明。
方法的本质
前面提到过,Go语言没有类,方法与类型通过receiver联系在一起。我们可以为任何非内置原生类型定义方法,比如下面的类型T:
type T struct {a int
}
func (t T) Get() int {return t.a
}
func (t *T) Set(a int) int {t.a = areturn t.a
}
C++的对象在调用方法时,编译器会自动传入指向对象自身的this指针作为方法的第一个参数。而对于Go来说,receiver其实也是同样道理,我们将receiver作为第一个参数传入方法的参数列表。
上面示例中类型T的方法可以等价转换为下面的普通函数:
func Get(t T) int {return t.a
}
func Set(t *T, a int) int {
t.a = areturn t.a
}
这种转换后的函数就是方法的原型。只不过在Go语言中,这种等价转换是由Go编译器在编译和生成代码时自动完成的。Go语言规范中提供了一个新概念,可以让我们更充分地理解上面的等价转换。
Go方法的一般使用方式如下:
var t T
t.Get()
t.Set(1)
我们可以用如下方式等价替换上面的方法调用:
var t T
T.Get(t)
(*T).Set(&t, 1)
这种直接以类型名T调用方法的表达方式被称为方法表达式(Method Expression)。类型T只能调用T的方法集合(Method Set)中的方法,同理,T只能调用T的方法集合中的方法。
这种通过方法表达式对方法进行调用的方式与我们之前所做的方法到函数的等价转换如出一辙。这就是Go方法的本质:一个以方法所绑定类型实例为第一个参数的普通函数。
Go方法自身的类型就是一个普通函数,我们甚至可以将其作为右值赋值给函数类型的变量:
var t T
f1 := (*T).Set // f1的类型,也是T类型Set方法的原型:func (t *T, int)int
f2 := T.Get // f2的类型,也是T类型Get方法的原型:func (t T)int
f1(&t, 3)
fmt.Println(f2(t))
选择正确的receiver类型
有了上面对Go方法本质的分析,再来理解receiver并在定义方法时选择正确的receiver类型就简单多了。我们看一下方法和函数的等价变换公式:
func (t T) M1() <=> M1(t T)
func (t *T) M2() <=> M2(t *T)
我们看到,M1方法的receiver参数类型为T,而M2方法的receiver参数类型为*T。
1)当receiver参数的类型为T时,选择值类型的receiver
选择以T作为receiver参数类型时,T的M1方法等价为M1(t T)。Go函数的参数采用的是值复制传递,也就是说M1函数体中的t是T类型实例的一个副本,这样在M1函数的实现中对参数t做任何修改都只会影响副本,而不会影响到原T类型实例。
2)当receiver参数的类型为*T时,选择指针类型的receiver
选择以*T作为receiver参数类型时,T的M2方法等价为M2(t *T)。我们传递给M2函数的t是T类型实例的地址,这样M2函数体中对参数t做的任何修改都会反映到原T类型实例上。
以下面的例子演示一下选择不同的receiver类型对原类型实例的影响:
type T struct {a int
}
func (t T) M1() {t.a = 10
}
func (t *T) M2() {t.a = 11
}
func main() {var t T // t.a = 0println(t.a)t.M1()println(t.a)t.M2()println(t.a)
}
运行该程序:
0
0
11
在该示例中,M1和M2方法体内都对字段a做了修改,但M1(采用值类型receiver)修改的只是实例的副本,对原实例并没有影响,因此M1调用后,输出t.a的值仍为0。而M2(采用指针类型receiver)修改的是实例本身,因此M2调用后,t.a的值变为了11。
很多Go初学者还有这样的疑惑:是不是T类型实例只能调用receiver为T类型的方法,不能调用receiver为*T类型的方法呢?答案是否定的。无论是T类型实例还是T类型实例,都既可以调用receiver为T类型的方法,也可以调用receiver为T类型的方法。
下面的例子证明了这一点:
package maintype T struct {a int
}func (t T) M1() {}
func (t *T) M2() {t.a = 11
}
func main() {var t Tt.M1() // okt.M2() // <=> (&t).M2()var pt = &T{}pt.M1() // <=> (*pt).M1()pt.M2() // ok
}
我们看到,T类型实例t调用receiver类型为T的M2方法是没问题的,同样T类型实例pt调用receiver类型为T的M1方法也是可以的。实际上这都是Go语法糖,Go编译器在编译和生成代码时为我们自动做了转换。
到这里,我们可以得出receiver类型选用的初步结论。
● 如果要对类型实例进行修改,那么为receiver选择*T类型。
● 如果没有对类型实例修改的需求,那么为receiver选择T类型或*T类型均可;但考虑到Go方法调用时,receiver是以值复制的形式传入方法中的,如果类型的size较大,以值形式传入会导致较大损耗,这时选择*T作为receiver类型会更好些
。
基于对Go方法本质的理解巧解难题
package mainimport ("fmt""time"
)type field struct {name string
}func (p *field) print() {fmt.Println(p.name)
}func main() {data1 := []*field{{"one"}, {"two"}, {"three"}}for _, v := range data1 {go v.print()}data2 := []field{{"four"}, {"five"}, {"six"}}for _, v := range data2 {go v.print()}time.Sleep(3 * time.Second)
}
运行结果如下(由于goroutine调度顺序不同,结果可能有差异):
one
two
three
six
six
six
为 什 么 对 data2 迭 代 输 出 的 结 果 是 3 个“six”, 而 不是“four”“five” “six”?
好了,我们来分析一下。首先,根据Go方法的本质——一个以方法所绑定类型实例为第一个参数的普通函数,对这个程序做个等价变换(这里我们利用方法表达式),变换后的源码如下:
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go (*field).print(v)
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go (*field).print(&v)
}
time.Sleep(3 * time.Second)
}
这里我们把对类型field的方法print的调用替换为方法表达式的形式,替换前后的程序输出结果是一致的。变换后,是不是感觉豁然开朗了?我们可以很清楚地看到使用go关键字启动一个新goroutine时是如何绑定参数的:
● 迭代data1时,由于data1中的元素类型是field指针(*field),因此赋值后v就是元素地址,每次调用print时传入的参数(v)实际上也是各个field元素的地址;
● 迭代data2时,由于data2中的元素类型是field(非指针),需要将其取地址后再传入。这样每次传入的&v实际上是变量v的地址,而不是切片data2中各元素的地址。
在第19条中,我们了解过for range使用时应注意的几个关键问题,其中就包括循环变量复用。这里的v在整个for range过程中只有一个,因此data2迭代完成之后,v是元素“six”的副本。
这样,一旦启动的各个子goroutine在main goroutine执行到Sleep时才被调度执行,那么最后的三个goroutine在打印&v时,打印的也就都是v中存放的值“six”了。而前三个子goroutine各自传入的是元素“one”“two”“three”的地址,打印的就是“one”“two”“three”了。
那 么 如 何 修 改 原 程 序 才 能 让 其 按 期 望 输 出(“one”“two”“three”“four”“five”“six”)呢?其实只需将field类型print
方法的receiver类型由*field改为field即可。
Go语言未提供对经典面向对象机制的语法支持,但实现了类型的方法,方法与类型间通过方法名左侧的receiver建立关联。为类型的方法选择合适的receiver类型是Gopher为类型定义方法的重要环节。
相关文章:
Go基础18-理解方法的本质以选择正确的receiver类型
Go语言虽然不支持经典的面向对象语法元素,比如类、对象、继承等,但Go语言也有方法。和函数相比,Go语言中的方法在声明形式上仅仅多了一个参数,Go称之为receiver参数。receiver参数是方法与类型之间的纽带。 Go方法的一般声明形式…...
Go基础12-理解Go语言表达式的求值顺序
Go语言在变量声明、初始化以及赋值语句上相比其先祖C语言做了一些改进,诸如: ● 支持在同一行声明和初始化多个变量(不同类型也可以) var a, b, c 5, "hello", 3.45 a, b, c : 5, "hello", 3.45 // 短变量…...

OJ练习第165题——修车的最少时间
修车的最少时间 力扣链接:2594. 修车的最少时间 题目描述 给你一个整数数组 ranks ,表示一些机械工的 能力值 。ranksi 是第 i 位机械工的能力值。能力值为 r 的机械工可以在 r * n2 分钟内修好 n 辆车。 同时给你一个整数 cars ,表示总…...

纯前端实现 导入 与 导出 Excel
最近经常在做 不规则Excel的导入,或者一些普通Excel的导出,当前以上说的都是纯前端来实现;下面我们来聊聊经常用到的Excel导出与导入的实现方案,本文实现技术栈以 Vue2 JS 为例 导入分类: 调用 API 完全由后端来解析数…...
关于一次两段式提交和数据库恢复数据我的一些想法
binlog是服务层的功能,而redolog是innodb引擎的功能,binlog主要用于主从复制,redolog主要用做数据的恢复,我们必须保证binlog和redolog日志数据的一致性。恢复数据时也必须遵守此一致性。 1.如果只写一次redolog会出现什么问题&a…...
阿里巴巴springcloud的gateway网关如何用继承接口WebExceptionHandler定义一个json格式的404错误页面实例
如果你想通过实现 WebExceptionHandler 接口来定义一个返回 JSON 格式的 404 错误页面的实例,可以按照以下方式操作: import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.ster…...
『力扣每日一题07』字符串最后一个单词的长度
气死我啦,今天这道题花了快一个小时,我学完了答案的解法,放上去在线 OJ ,一直报错,找来找去都找不到自己错在哪,明明跟答案一模一样。后来还是学了另一种解法,才跑出来的(̥̥̥̥̥̥̥̥o̥̥…...

成都睿趣科技:抖音开店初期要注意什么
随着社交媒体和短视频平台的崛起,抖音已经成为了一个风靡全球的短视频应用,拥有着庞大的用户群体。因此,越来越多的创业者开始在抖音上开设自己的线上店铺,希望借助这个平台赚取丰厚的利润。然而,在抖音开店初期&#…...
QT 5.13保姆级安装教程
辨清关系 要想学习一个新的东西,我们必须知其事,达其理,悟其道,然后才能无往而不利也! 我们常听到QT、Qt Creator 和 Qt SDK ,这三者究竟是什么,他们之间的关系又是如何的?在安装QT之前我们先来了解一下他们之间的关系: Qt:Qt 是一个跨平台的 C++ 应用程序开发框架,…...
js 创建DOM,并添加父DOM上,移除某个DOM的所有子节点
在sectionIdDiv上,添加子DOM <div ref"sectionIdDiv" class"sectionIdDiv"> </div>创建你要添加的子DOM ## 创建DOM let elementDom document.createElement(div)## 设置DOM的样式 elementDom.style.height "15px" e…...

element el-input 二次封装
说明:为实现输入限制,不可输入空格,长度限制。 inputView.vue <template><!-- 输入框 --><el-input:type"type":placeholder"placeholder"v-model"input"input"inputChange":maxle…...

[源码系列:手写spring] IOC第十三节:Bean作用域,增加prototype的支持
为了帮助大家更深入的理解bean的作用域,特意将BeanDefinition的双例支持留到本章节中,创建Bean,相关Reader读取等逻辑都有所改动。 内容介绍 在Spring中,Bean的作用域(Scope)定义了Bean的生命周期和可见性。包括单例和…...
【性能优化】事件委托
一、为什么要用事件委托 当 dom 有事件处理程序时,我们一般都会直接给它设置事件处理程序,设想一下,如果在一个父元素中有很多个 dom 需要添加事件处理呢?比如 ul 中处在100个 li,每个 li 都有相同的 click 事件&…...

C 风格文件输入/输出---无格式输入/输出---(std::fputc,std::putc,std::fputs)
C 标准库的 C I/O 子集实现 C 风格流输入/输出操作。 <cstdio> 头文件提供通用文件支持并提供有窄和多字节字符输入/输出能力的函数,而 <cwchar>头文件提供有宽字符输入/输出能力的函数。 无格式输入/输出 写字符到文件流 std::fputc, std::putc in…...
建议收藏!Harmony应用配置文件概述(Stage模型)
一. 应用配置文件 每个应用项目必须在项目的代码目录下加入配置文件,这些配置文件会向编译工具、操作系统和应用市场提供应用的基本信息。 在基于Stage模型开发的应用项目代码下,都存在一个app.json5及一个或多个module.json5这两种配置文件。 app.json5…...

金蝶云星空和四化智造MES(WEB)单据接口对接
金蝶云星空和四化智造MES(WEB)单据接口对接 接入系统:四化智造MES(WEB) MES建立统一平台上通过物料防错防错、流程防错、生产统计、异常处理、信息采集和全流程追溯等精益生产和精细化管理,帮助企业合理安排…...

Shell命令切换root用户、管理配置文件、检查硬件
Shell命令切换root用户、管理配置文件、检查硬件 切换root用户 两种方法 su命令详细介绍 sudo命令详细介绍 /etc/passwd文件 /etc/passwd文件里为什么有乱七八糟的用户? /etc/shadow文件 管理配置文件 检查硬件命令 查看CPU 查看GPU 与其他基于UNIX的系统…...

DataX(MySQL同步数据到Doris)
1.场景 这里演示介绍的使用 Doris 的 Datax 扩展 DorisWriter实现从Mysql数据定时抽取数据导入到Doris数仓表里 2.编译 DorisWriter 这个的扩展的编译可以不在 doris 的 docker 编译环境下进行,本文是在 windows 下的 WLS 下进行编译的 首先从github上拉取源码 …...

sql server服务无法启动怎么办?如何正常启动?
sql server软件是一款关系型数据库管理系统。具有使用方便可伸缩性好与相关软件集成程度高等优点。并且有些应用软件使用过程中是需要sql server数据库的后台支持的,我们在数据编程操作时经常会使用这款编程软件,在编程时系统有时会提示sql server服务无…...

SpringMVC实现文件上传和下载
目录 前言 一. SpringMVC文件上传 1. 配置多功能视图解析器 2. 前端代码中,将表单标记为多功能表单 3. 后端利用MultipartFile 接口,接收前端传递到后台的文件 4. 文件上传示例 1. 相关依赖: 2. 逆向生成对应的类 3. 后端代码…...

华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...

【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...

51c自动驾驶~合集58
我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留,CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制(CCA-Attention),…...

【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...

C++使用 new 来创建动态数组
问题: 不能使用变量定义数组大小 原因: 这是因为数组在内存中是连续存储的,编译器需要在编译阶段就确定数组的大小,以便正确地分配内存空间。如果允许使用变量来定义数组的大小,那么编译器就无法在编译时确定数组的大…...
#Uniapp篇:chrome调试unapp适配
chrome调试设备----使用Android模拟机开发调试移动端页面 Chrome://inspect/#devices MuMu模拟器Edge浏览器:Android原生APP嵌入的H5页面元素定位 chrome://inspect/#devices uniapp单位适配 根路径下 postcss.config.js 需要装这些插件 “postcss”: “^8.5.…...

让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比
在机器学习的回归分析中,损失函数的选择对模型性能具有决定性影响。均方误差(MSE)作为经典的损失函数,在处理干净数据时表现优异,但在面对包含异常值的噪声数据时,其对大误差的二次惩罚机制往往导致模型参数…...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...

视觉slam十四讲实践部分记录——ch2、ch3
ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...