Go 方法介绍,理解“方法”的本质

news2025/1/19 2:33:17

Go 方法介绍,理解“方法”的本质

文章目录

  • Go 方法介绍,理解“方法”的本质
    • 一、认识 Go 方法
      • 1.1 基本介绍
      • 1.2 声明
      • 1.2.1 引入
      • 1.2.2 一般声明形式
      • 1.2.3 receiver 参数作用域
      • 1.2.4 receiver 参数的基类型约束
      • 1.2.5 方法声明的位置约束
      • 1.2.6 如何使用方法
    • 二、方法的本质
    • 三、巧解难题

一、认识 Go 方法

1.1 基本介绍

我们知道,Go 语言从设计伊始,就不支持经典的面向对象语法元素,比如类、对象、继承,等等,但 Go 语言仍保留了名为“方法(method)”的语法元素。当然,Go 语言中的方法和面向对象中的方法并不是一样的。Go 引入方法这一元素,并不是要支持面向对象编程范式,而是 Go 践行组合设计哲学的一种实现层面的需要。

在 Go 编程语言中,方法是与特定类型相关联的函数。它们允许您在自定义类型上定义行为,这个自定义类型可以是结构体(struct)或任何用户定义的类型。方法本质上是一种函数,但它们具有一个特定的接收者(receiver),也就是方法所附加到的类型。这个接收者可以是指针类型或值类型。方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

1.2 声明

1.2.1 引入

首先我们这里以 Go 标准库 net/http 包中 *Server 类型的方法 ListenAndServeTLS 为例,讲解一下 Go 方法的一般形式:

img

和 Go 函数一样,Go 的方法也是以 func 关键字修饰的,并且和函数一样,也包含方法名(对应函数名)、参数列表、返回值列表与方法体(对应函数体)。

而且,方法中的这几个部分和函数声明中对应的部分,在形式与语义方面都是一致的,比如:方法名字首字母大小写决定该方法是否是导出方法;方法参数列表支持变长参数;方法的返回值列表也支持具名返回值等。

不过,它们也有不同的地方。从上面这张图我们可以看到,和由五个部分组成的函数声明不同,Go 方法的声明有六个组成部分,多的一个就是图中的 receiver 部分。在 receiver 部分声明的参数,Go 称之为 receiver 参数,这个 receiver 参数也是方法与类型之间的纽带,也是方法与函数的最大不同。

Go 中的方法必须是归属于一个类型的,而 receiver 参数的类型就是这个方法归属的类型,或者说这个方法就是这个类型的一个方法。以图中的 ListenAndServeTLS 为例,这里的 receiver 参数 srv 的类型为 *Server,那么我们可以说,这个方法就是 *Server 类型的方法。

注意!这里说的是 ListenAndServeTLS*Server 类型的方法,而不是 Server 类型的方法。

1.2.2 一般声明形式

方法的声明形式如下:

func (t *T或T) MethodName(参数列表) (返回值列表) {
    // 方法体
}

其中各部分的含义如下:

  • (t *T或T):括号中的部分是方法的接收者,用于指定方法将附加到的类型。t 是接收者的名称,T 是接收者的类型。接收者可以是值类型(T)或指针类型(*T)。如果使用值类型作为接收者,方法操作的是接收者的副本,而指针类型允许方法修改接收者的原始值。无论 receiver 参数的类型为 *T 还是 T,我们都把一般声明形式中的 T 叫做 receiver 参数 t 的基类型。如果 t 的类型为 T,那么说这个方法是类型 T 的一个方法;如果 t 的类型为 *T,那么就说这个方法是类型 *T 的一个方法。而且,要注意的是,每个方法只能有一个 receiver 参数,Go 不支持在方法的 receiver 部分放置包含多个 receiver 参数的参数列表,或者变长 receiver 参数。
  • MethodName:这是方法的名称,用于在调用方法时引用它。
  • (参数列表):这是方法的参数列表,定义了方法可以接受的参数。如果方法不需要参数,此部分为空。
  • (返回值列表):这是方法的返回值列表,定义了方法返回的结果。如果方法不返回任何值,此部分为空。
  • 方法体:方法体包含了方法的具体实现,这里可以编写方法的功能代码。

1.2.3 receiver 参数作用域

方法接收器(receiver)参数、函数 / 方法参数,以及返回值变量对应的作用域范围,都是函数 / 方法体对应的显式代码块。

这就意味着,receiver 部分的参数名不能与方法参数列表中的形参名,以及具名返回值中的变量名存在冲突,必须在这个方法的作用域中具有唯一性。如果不唯一,比如下面的例子中那样,Go 编译器就会报错:

type T struct{}

func (t T) M(t string) { // 编译器报错:duplicate argument t (重复声明参数t)
    ... ...
}

不过,如果在方法体中没有使用 receiver 参数,我们也可以省略 receiver 的参数名,就像下面这样:

type T struct{}

func (T) M(t string) { 
    ... ...
}

仅当方法体中的实现不需要 receiver 参数参与时,我们才会省略 receiver 参数名,不过这一情况很少使用,了解一下即可。

1.2.4 receiver 参数的基类型约束

Go 语言对 receiver 参数的基类型也有约束,那就是 receiver 参数的基类型本身不能为指针类型或接口类型。

下面的例子分别演示了基类型为指针类型和接口类型时,Go 编译器报错的情况:

type MyInt *int
func (r MyInt) String() string { // r的基类型为MyInt,编译器报错: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) { // r的基类型为MyReader,编译器报错:invalid receiver type MyReader (MyReader is an interface type)
    return r.Read(p)
}

1.2.5 方法声明的位置约束

Go 要求,方法声明要与 receiver 参数的基类型声明放在同一个包内。基于这个约束,我们还可以得到两个推论。

  • 第一个推论:我们不能为原生类型(例如 int、float64、map 等)添加方法。例如,下面的代码试图为 Go 原生类型 int 增加新方法 Foo,这是不允许的,Go 编译器会报错:
func (i int) Foo() string { // 编译器报错:cannot define new methods on non-local type int
    return fmt.Sprintf("%d", i) 
}
  • 第二个推论:不能跨越 Go 包为其他包的类型声明新方法。例如,下面的代码试图跨越包边界,为 Go 标准库中的 http.Server 类型添加新方法 Foo,这是不允许的,Go 编译器同样会报错:
import "net/http"

func (s http.Server) Foo() { // 编译器报错:cannot define new methods on non-local type http.Server
}

1.2.6 如何使用方法

我们直接还是通过一个例子理解一下。如果 receiver 参数的基类型为 T,那么我们说 receiver 参数绑定在 T 上,我们可以通过 *T 或 T 的变量实例调用该方法:

type T struct{}

func (t T) M(n int) {
}

func main() {
    var t T
    t.M(1) // 通过类型T的变量实例调用方法M

    p := &T{}
    p.M(2) // 通过类型*T的变量实例调用方法M
}

这段代码中,方法 M 是类型 T 的方法,通过 *T 类型变量也可以调用 M 方法。

二、方法的本质

通过以上,我们知道了 Go 的方法与 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 = a 
    return t.a 
}

在Go 中,Go 方法中的原理是将 receiver 参数以第一个参数的身份并入到方法的参数列表中。按照这个原理,我们示例中的类型 T*T 的方法,就可以分别等价转换为下面的普通函数:

// 类型T的方法Get的等价函数
func Get(t T) int {  
    return t.a 
}

// 类型*T的方法Set的等价函数
func Set(t *T, a int) int { 
    t.a = a 
    return t.a 
}

这种等价转换后的函数的类型就是方法的类型。只不过在 Go 语言中,这种等价转换是由 Go 编译器在编译和生成代码时自动完成的。Go 语言规范中还提供了方法表达式(Method Expression)的概念,可以让我们更充分地理解上面的等价转换。

以上面类型 T 以及它的方法为例,结合前面说过的 Go 方法的调用方式,我们可以得到下面代码:

var t T
t.Get()
(&t).Set(1)

我们可以用另一种方式,把上面的方法调用做一个等价替换:

var t T
T.Get(t)
(*T).Set(&t, 1)

这种直接以类型名 T 调用方法的表达方式,被称为Method Expression。通过Method Expression这种形式,类型 T 只能调用 T 的方法集合(Method Set)中的方法,同理类型 *T 也只能调用 *T 的方法集合中的方法。

我们看到,Method Expression 有些类似于 C++ 中的静态方法(Static Method)。在 C++ 中的静态方法使用时,以该 C++ 类的某个对象实例作为第一个参数。而 Go 语言的 Method Expression 在使用时,同样以 receiver 参数所代表的类型实例作为第一个参数。

这种通过 Method Expression 对方法进行调用的方式,与我们之前所做的方法到函数的等价转换是如出一辙的。所以,Go 语言中的方法的本质就是,一个以方法的 receiver 参数作为第一个参数的普通函数。

而且,Method Expression 就是 Go 方法本质的最好体现,因为方法自身的类型就是一个普通函数的类型,我们甚至可以将它作为右值,赋值给一个函数类型的变量,比如下面示例:

func main() {
    var t T
    f1 := (*T).Set // f1的类型,也是*T类型Set方法的类型:func (t *T, int)int
    f2 := T.Get    // f2的类型,也是T类型Get方法的类型:func(t T)int
    fmt.Printf("the type of f1 is %T\n", f1) // the type of f1 is func(*main.T, int) int
    fmt.Printf("the type of f2 is %T\n", f2) // the type of f2 is func(main.T) int
    f1(&t, 3)
    fmt.Println(f2(t)) // 3
}

三、巧解难题

我们来看一段代码:

package main

import (
    "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)
}

这段代码在我的多核 macOS 上的运行结果是这样(由于 Goroutine 调度顺序不同,你自己的运行结果中的行序可能与下面的有差异):

one
two
three
six
six
six

为什么对 data2 迭代输出的结果是三个“six”,而不是 four、five、six?

我们来分析一下。首先,我们根据 Go 方法的本质,也就是一个以方法的 receiver 参数作为第一个参数的普通函数,对这个程序做个等价变换。这里我们利用 Method Expression 方式,等价变换后的源码如下:

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 的调用,替换为 Method Expression 形式,替换前后的程序输出结果是一致的。但变换后,问题是不是豁然开朗了!我们可以很清楚地看到使用 go 关键字启动一个新 Goroutine 时,Method Expression 形式的 print 函数是如何绑定参数的:

  • 迭代 data1 时,由于 data1 中的元素类型是 field 指针 (*field),因此赋值后 v 就是元素地址,与 printreceiver 参数类型相同,每次调用 (*field).print 函数时直接传入的 v 即可,实际上传入的也是各个 field 元素的地址。
  • 迭代 data2 时,由于 data2 中的元素类型是 field(非指针),与 printreceiver 参数类型不同,因此需要将其取地址后再传入 (*field).print 函数。这样每次传入的 &v 实际上是变量 v 的地址,而不是切片 data2 中各元素的地址。

在《Go 的 for 循环,仅此一种》中,我们学习过 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 就可以了。我们直接来看一下修改后的代码:

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
four
five
six

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

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

相关文章

uniapp: 前端利用百度云OCR实现文字识别(身份证识别功能,别的功能类似)

第一章 前言 介绍如何使用百度智能云实现我们想要的效果,需要在下面这个网址注册账号: 百度智能云-云智一体深入产业 使用文档在该网址上: 简介 - 文字识别OCR 请求成功的效果,如下图: 搜索产品(例如文字…

数据结构--前缀树(Trie)

1. 简介 前缀树是一种数据结构&#xff0c;常用来字符搜索。 2. 实现 包含的操作主要是: 加入串搜索串 代码实现&#xff0c;直接用leetcode_208的题解咯。 代码 class Trie { public:Trie():isEnd(false){for ( int i 0; i < 26;i)child[i] nullptr;}~Trie() {fo…

零基础搭建Nextcloud私有云盘并通过内网穿透实现远程访问

文章目录 摘要1. 环境搭建2. 测试局域网访问3. 内网穿透3.1 ubuntu本地安装cpolar3.2 创建隧道3.3 测试公网访问 4 配置固定http公网地址4.1 保留一个二级子域名4.1 配置固定二级子域名4.3 测试访问公网固定二级子域名 摘要 Nextcloud,它是ownCloud的一个分支,是一个文件共享服…

Vue 条件渲染 与 列表渲染

目录 一、条件渲染 1.简介 : 2.v-if实例 : 3.v-show实例 : 4.v-if与v-show的区别 : 二、列表渲染 1.基本用法 : 1.1 v-for遍历数组 1.2 v-for遍历对象 2.应用实例 : 一、条件渲染 1.简介 : (1) Vue提供了v-if 和 v-show条件指令来完成条件渲染/控制。 v-if指令用于条…

​C++内存模型

c语言分区:栈、堆、全局/静态存储区、常量存储区、代码区(.text段)、自由存储区 1、栈区&#xff08;stack&#xff09;— 由编译器自动分配释放&#xff0c;存放函数的参数值&#xff0c;局部变量的值等。其操作方式类似于数据结构中的栈。向下生长 2、堆区&#xff08;heap&…

iSlide2024一款基于PPT的插件工具包含38个设计辅助功能

根据使用者情况表明iSlide 是一款拥有30W素材的PPT高效设计软件&#xff0c;可提高90%工作效率&#xff0c;现全球已有超过1400万使用者&#xff0c;智能排版原创高品模板可商用图形&#xff0c;真正摆脱PPT的束缚&#xff0c;把精力用在该用的地方。我们都明白islide插件功能特…

Linux背景介绍与环境搭建

本章内容 认识 Linux, 了解 Linux 的相关背景学会如何使用云服务器掌握使用远程终端工具 xshell 登陆 Linux 服务器 Linux 背景介绍 发展史 本门课程学习Linux系统编程&#xff0c;你可能要问Linux从哪里来&#xff1f;它是怎么发展的&#xff1f;在这里简要介绍Linux的发展…

pytorch 中 nn.Conv2d 解释

1. pytorch nn.Con2d 中填充模式 torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride1, padding0, dilation1, groups1, biasTrue, padding_mode‘zeros’, deviceNone, dtypeNone) 1.1 padding 参数的含义 首先 &#xff0c;padd N, 代表的是 分别在 上下&…

阿里云安全恶意程序检测

阿里云安全恶意程序检测 赛题理解赛题介绍赛题说明数据说明评测指标 赛题分析数据特征解题思路 数据探索数据特征类型数据分布箱型图 变量取值分布缺失值异常值分析训练集的tid特征标签分布测试集数据探索同上 数据集联合分析file_id分析API分析 特征工程与基线模型构造特征与特…

driver.find_element()用法

driver.find_element()用于在Web页面中定位单个元素。它是Selenium WebDriver库中的 一种方法。该方法接受一个定位器&#xff08;locator&#xff09;和一个值作为参数&#xff0c;用于指定要查找的元素 位置。下面是具体的用法和一些例子&#xff1a; 通过ID定位元素&#x…

AI神助攻,购物更省心:我即将上线一套企业数据高度契合的智能导购APP来开创这一新纪元

将要做什么事的介绍 近期博客写了少了&#xff0c;是因为近小半年来我正在打造一款可私布在企业内部并结合企业自有领域&#xff08;零售商超先行&#xff09;数据的智能导购引擎。截止目前为止还算顺利&#xff0c;并且我将很快将在中国本土的一家生鲜百货超市上线这一款生成…

[iOS开发]iOS中TabBar中间按钮凸起的实现

在日常使用app的过程中&#xff0c;经常能看到人家实现了底部分栏控制器的中间按钮凸起的效果&#xff0c;那么这是怎么实现的呢&#xff1f; 效果演示&#xff1a; 实现原理&#xff1a; 创建按钮 创建一个UITabBar的子类&#xff0c;重写它的layoutSubviews方法&#xff1…

redis源码分析之IO多路复用

文章目录 1、简述2、多路复用的三个函数3、创建epoll实例4、绑定端口、监听端口5、向epoll实例注册连接事件6、从epoll实例中获取就绪的事件 1、简述 众所周知&#xff0c;redis是一款抗高并发的利器&#xff0c;据官方压测&#xff0c;单机可达10万qps。但背后实际处理命令的…

字典与数组第八讲:工作表数据计算时为什么要采用数组公式(二)

《VBA数组与字典方案》教程&#xff08;10144533&#xff09;是我推出的第三套教程&#xff0c;目前已经是第二版修订了。这套教程定位于中级&#xff0c;字典是VBA的精华&#xff0c;我要求学员必学。7.1.3.9教程和手册掌握后&#xff0c;可以解决大多数工作中遇到的实际问题。…

具有自主产权的SaaS门店收银系统全套源码输出

PHPMysql前后端分离&#xff0c; 小程序线上商城&#xff1b; 进销存管理库存盘点&#xff0c; 多仓库库存调拨&#xff0c; 会员系统。 消费者扫码查价系统。

外卖行业如何借助微信管理系统实现高效运营

摘要&#xff1a;本文将介绍微信管理系统在外卖行业的应用&#xff0c;包括聚合聊天、朋友圈营销和群发功能。通过这些功能&#xff0c;外卖商家可以更高效地管理订单、与客户沟通、推广品牌和增加销售额。 一、引言 随着外卖行业的快速发展&#xff0c;竞争也日益激烈。为了…

前端面试题之HTML篇

1、src 和 href 的区别 具有src的标签有&#xff1a;script、img、iframe 具有href的标签有&#xff1a;link、a 区别 src 是source的缩写。表示源的意思&#xff0c;指向资源的地址并下载应用到文档中。会阻塞文档的渲染&#xff0c;也就是为什么js脚本放在底部而不是头部的…

Vert.x学习笔记-Vert.x的基本处理单元Verticle

Verticle介绍 Verticle是Vert.x的基本处理单元&#xff0c;Vert.x应用程序中存在着处理各种事件的处理单元&#xff0c;比如负责HTTP API响应请求的处理单元、负责数据库存取的处理单元、负责向第三方发送请求的处理单元。Verticle就是对这些功能单元的封装&#xff0c;Vertic…

数据中心系统解决方案

设计思路 系统设计过程中充分考虑各个子系统的信息共享要求&#xff0c;对各子系统进行结构化和标准化设计&#xff0c;通过系统间的各种联动方式将其整合成一个有机的整体&#xff0c;使之成为一套整体的、全方位的数据中心大楼综合管理系统&#xff0c;达到人防、物防和技防…

MySQL8.0.26-unbuntu版安装

MySQL8.0.26-ubuntu版安装 在这里会有一个坑&#xff0c;就是我在安装的时候,是按照另外一种版本的安装&#xff0c;报错没有rpm这个包&#xff0c;然后我就去下载&#xff0c;然后就报错 E: 无法定位软件包 &#xff0c;害的我找了好久的资料&#xff0c;一直没有解决&#x…