当前位置: 首页 > news >正文

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语言虽然不支持经典的面向对象语法元素&#xff0c;比如类、对象、继承等&#xff0c;但Go语言也有方法。和函数相比&#xff0c;Go语言中的方法在声明形式上仅仅多了一个参数&#xff0c;Go称之为receiver参数。receiver参数是方法与类型之间的纽带。 Go方法的一般声明形式…...

Go基础12-理解Go语言表达式的求值顺序

Go语言在变量声明、初始化以及赋值语句上相比其先祖C语言做了一些改进&#xff0c;诸如&#xff1a; ● 支持在同一行声明和初始化多个变量&#xff08;不同类型也可以&#xff09; var a, b, c 5, "hello", 3.45 a, b, c : 5, "hello", 3.45 // 短变量…...

OJ练习第165题——修车的最少时间

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

纯前端实现 导入 与 导出 Excel

最近经常在做 不规则Excel的导入&#xff0c;或者一些普通Excel的导出&#xff0c;当前以上说的都是纯前端来实现&#xff1b;下面我们来聊聊经常用到的Excel导出与导入的实现方案&#xff0c;本文实现技术栈以 Vue2 JS 为例 导入分类&#xff1a; 调用 API 完全由后端来解析数…...

关于一次两段式提交和数据库恢复数据我的一些想法

binlog是服务层的功能&#xff0c;而redolog是innodb引擎的功能&#xff0c;binlog主要用于主从复制&#xff0c;redolog主要用做数据的恢复&#xff0c;我们必须保证binlog和redolog日志数据的一致性。恢复数据时也必须遵守此一致性。 1.如果只写一次redolog会出现什么问题&a…...

阿里巴巴springcloud的gateway网关如何用继承接口WebExceptionHandler定义一个json格式的404错误页面实例

如果你想通过实现 WebExceptionHandler 接口来定义一个返回 JSON 格式的 404 错误页面的实例&#xff0c;可以按照以下方式操作&#xff1a; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.ster…...

『力扣每日一题07』字符串最后一个单词的长度

气死我啦&#xff0c;今天这道题花了快一个小时&#xff0c;我学完了答案的解法&#xff0c;放上去在线 OJ &#xff0c;一直报错&#xff0c;找来找去都找不到自己错在哪&#xff0c;明明跟答案一模一样。后来还是学了另一种解法&#xff0c;才跑出来的(̥̥̥̥̥̥̥̥o̥̥…...

成都睿趣科技:抖音开店初期要注意什么

随着社交媒体和短视频平台的崛起&#xff0c;抖音已经成为了一个风靡全球的短视频应用&#xff0c;拥有着庞大的用户群体。因此&#xff0c;越来越多的创业者开始在抖音上开设自己的线上店铺&#xff0c;希望借助这个平台赚取丰厚的利润。然而&#xff0c;在抖音开店初期&#…...

QT 5.13保姆级安装教程

辨清关系 要想学习一个新的东西,我们必须知其事,达其理,悟其道,然后才能无往而不利也! 我们常听到QT、Qt Creator 和 Qt SDK ,这三者究竟是什么,他们之间的关系又是如何的?在安装QT之前我们先来了解一下他们之间的关系: Qt:Qt 是一个跨平台的 C++ 应用程序开发框架,…...

js 创建DOM,并添加父DOM上,移除某个DOM的所有子节点

在sectionIdDiv上&#xff0c;添加子DOM <div ref"sectionIdDiv" class"sectionIdDiv"> </div>创建你要添加的子DOM ## 创建DOM let elementDom document.createElement(div)## 设置DOM的样式 elementDom.style.height "15px" e…...

element el-input 二次封装

说明&#xff1a;为实现输入限制&#xff0c;不可输入空格&#xff0c;长度限制。 inputView.vue <template><!-- 输入框 --><el-input:type"type":placeholder"placeholder"v-model"input"input"inputChange":maxle…...

[源码系列:手写spring] IOC第十三节:Bean作用域,增加prototype的支持

为了帮助大家更深入的理解bean的作用域&#xff0c;特意将BeanDefinition的双例支持留到本章节中&#xff0c;创建Bean,相关Reader读取等逻辑都有所改动。 内容介绍 在Spring中&#xff0c;Bean的作用域&#xff08;Scope&#xff09;定义了Bean的生命周期和可见性。包括单例和…...

【性能优化】事件委托

一、为什么要用事件委托 当 dom 有事件处理程序时&#xff0c;我们一般都会直接给它设置事件处理程序&#xff0c;设想一下&#xff0c;如果在一个父元素中有很多个 dom 需要添加事件处理呢&#xff1f;比如 ul 中处在100个 li&#xff0c;每个 li 都有相同的 click 事件&…...

C 风格文件输入/输出---无格式输入/输出---(std::fputc,std::putc,std::fputs)

C 标准库的 C I/O 子集实现 C 风格流输入/输出操作。 <cstdio> 头文件提供通用文件支持并提供有窄和多字节字符输入/输出能力的函数&#xff0c;而 <cwchar>头文件提供有宽字符输入/输出能力的函数。 无格式输入/输出 写字符到文件流 std::fputc, std::putc in…...

建议收藏!Harmony应用配置文件概述(Stage模型)

一. 应用配置文件 每个应用项目必须在项目的代码目录下加入配置文件&#xff0c;这些配置文件会向编译工具、操作系统和应用市场提供应用的基本信息。 在基于Stage模型开发的应用项目代码下&#xff0c;都存在一个app.json5及一个或多个module.json5这两种配置文件。 app.json5…...

金蝶云星空和四化智造MES(WEB)单据接口对接

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

Shell命令切换root用户、管理配置文件、检查硬件

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

DataX(MySQL同步数据到Doris)

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

sql server服务无法启动怎么办?如何正常启动?

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

SpringMVC实现文件上传和下载

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

Your build is currently configured to use Java 20.0.2 and Gradle 8.0

jdk 版本不适配 下载jdk17 https://www.oracle.com/java/technologies/downloads/#jdk17 参考 JDK17的下载安装与配置(详细教程)_keyila798的博客-CSDN博客...

栈 之 如何实现一个栈

前言 栈最鲜明的特点就是后进先出&#xff0c;一碟盘子就是类似这样的结构&#xff0c;最晚放上去的&#xff0c;可以最先拿出来。本文将介绍的是如何自己实现一个栈结构。 栈的操作 栈是一种先进后出&#xff08;Last-In-First-Out, LIFO&#xff09;的数据结构&#xff0c…...

uni-app:自带的消息提示被遮挡的解决办法(自定义消息提示框)

效果&#xff1a; 代码&#xff1a; 1、在最外层或者根组件的模板中添加一个容器元素&#xff0c;用于显示提示消息。例如&#xff1a; <div class"toast-container" v-if"toastMessage"><div class"toast-content">{{ toastMessa…...

PHP设备检验系统Dreamweaver开发mysql数据库web结构php编程计算机网页代码

一、源码特点 PHP设备检验系统是一套完善的web设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 下载地址 https://download.csdn.net/download/qq_41221322/88306259 php设备检验系统1 …...

Windows 可以使用以下快捷键打开终端(命令提示符)

Windows 可以使用以下快捷键打开终端&#xff08;命令提示符&#xff09; 使用快捷键 Win R 打开 “运行” 对话框&#xff0c;然后输入 “cmd” 并按下 Enter 键。这将打开默认的命令提示符窗口。 使用快捷键 Ctrl Shift Esc 打开任务管理器&#xff0c;然后在 “文件” …...

Netty编程面试题

1.Netty 是什么&#xff1f; Netty是 一个异步事件驱动的网络应用程序框架&#xff0c;用于快速开发可维护的高性能协议服务器和客户端。Netty是基于nio的&#xff0c;它封装了jdk的nio&#xff0c;让我们使用起来更加方法灵活。 2.Netty 的特点是什么&#xff1f; 高并发&a…...

math_review

topics mathmatics supreme and optimumNorm and Linear producttopology of R*Continuious Function supreme and optimum Def 1: 非空有界集合必有上确界 common norm (1) x ∈ \in ∈ Rn, ||x||2 x 1 2 x 2 2 . . . x n 2 \sqrt {x_1^2x_2^2...x_n^2} x12​x22​.…...

肖sir__设计测试用例方法之场景法04_(黑盒测试)

设计测试用例方法之场景法 1、场景法主要是针对测试场景类型的&#xff0c;顾也称场景流程分析法。 2、流程分析是将软件系统的某个流程看成路径&#xff0c;用路径分析的方法来设计测试用例。根据流程的顺序依次进行组合&#xff0c;使得流程的各个分支能走到。 举例说明&…...

plt函数显示图片 在图片上画边界框 边界框坐标转换

一.读取图片并显示图片 %matplotlib inline import torch from d2l import torch as d2l读取图片 image_path ../data/images/cat_dog_new.jpg # 创建画板 figure d2l.set_figsize() image d2l.plt.imread(image_path) d2l.plt.imshow(image);二.给出一个(x左上角,y左上角,…...

运行期获得文件名和行号

探索动态日志模块的实现 最初的目标是创建一个通用的日志模块, 它具有基本的日志输出功能并支持重定向. 这样, 如果需要更换日志模块, 可以轻松实现. 最初的构想是通过函数重定向, 即使用 dlsym 来重定向所有函数以实现打印功能. 然而, 这种方法引发了一个问题, 即无法正确获…...

做商城网站需要多少钱/国内永久免费云服务器

1:基本使用 A:创建Java项目&#xff1a; 点击File或者在最左侧空白处&#xff0c;选择Java项目&#xff0c;在界面中写一个项目名称&#xff0c;然后 Finish即可。 B:创建包&#xff1a;展开项目&#xff0c;在源包src下建立一个包com.itheima C:创建类&#xff1a;在com.ithie…...

网站做跳转付款/广州网站优化推广方案

...

怎么做网站开发/常州网络推广seo

通过使用数据库服务器端的sqlnet.ora文件可以实现禁止指定IP主机访问数据库的功能&#xff0c;这对于提升数据库的安全性有很大的帮助&#xff0c;与此同时&#xff0c;这个技术为我们管理和约束数据库访问控制提供了有效的手段。下面是实现这个目的的具体步骤仅供参考&#xf…...

可以做英语阅读理解的网站/比较正规的代运营

使用 Spring MVC 时&#xff0c;很多业务场景下 Controller 需要接收日期时间参数。一个简单的做法是使用 String 接收日期时间字符串(例如&#xff1a;2020-01-29)&#xff0c;然后在代码中将其转换成 Java 8 的日期时间类型或 java.util.Date 类型。这种方法虽然简单&#xf…...

桂林最新疫情最新消息封城/搜索引擎推广与优化

1. Java并发类: 1、ConcurrentHashMap 01、和HashMap功能基本一致&#xff0c;主要是为了解决HashMap线程不安全问题&#xff1b; 02、java7中的基本设计理念就是切分成多个Segment块&#xff0c; 默认是16个&#xff0c;也就是说并发度是16&#xff0c;可以初始化时显式指定…...

长沙网约车/沧州seo包年优化软件排名

自VMware View 4.5发布以后&#xff0c;无论是代理商还是客户在做完对比测试以后&#xff0c;几乎无一例外地告诉我&ldquo;View在局域网里比XenDesktop做得更好&#xff01;&rdquo;。但言外之意却是&ldquo;Citrix在广域网里比你们强&#xff01;&rdquo;而最经常…...