【Go语言圣经】第六节:方法
第六章:方法
6.1 方法声明
在函数声明时,在其名字之前放上一个变量,这就是声明了变量对应类型的一个方法,相当于为这种类型定义了一个独占的方法。
下例为 Point 类型声明了计算两个点之间距离的方法:
package mainimport "math"type Point struct {X, Y float64
}func (p Point) Distance(q Point) float64 {return math.Hypot(q.X - p.X, q.Y - p.Y)
}
上述代码中的 p,叫做方法的接收器(receiver)。在 Golang 当中,不会像其他语言那样使用 this 或 self 作为接收器,可以任意选择作为接收器的名字。
下例尝试使用方法:
package mainimport ("fmt""math"
)type Point struct {X, Y float64
}func (p Point) Distance(q Point) float64 {return math.Hypot(q.X-p.X, q.Y-p.Y)
}func main() {p := Point{1, 2}q := Point{4, 5}distance := p.Distance(q)fmt.Println(distance)
}
需要注意的是,此处的p.Distance是方法的调用,我们当然可以声明一个Distance同名函数,接受p和q作为参数,计算二者之间的距离。函数和方法是不冲突的,可以同时调用。
但是需要注意的话,如果我们声明了一个名为X的方法,编译器会报错,因为产生了歧义,因为X已经是Point的成员变量了。
下例定义了一个 Path 类型,Path 代表一个线段的集合,并且同样为 Path 定义一个名为 Distance 的方法:
type Path []Pointfunc (path Path) Distance() float64 {sum := 0.0for i := range path {if i > 0 {sum += path[i - 1].Distance((path[i]))}}return sum
}
Path 是一个命名的 slice 类型,但是我们仍然可以为其定义方法。在 Golang 当中,我们可以给同一个包内的任意命名类型定义方法,只要这个命名类型的底层类型不是指针或 interface。
下例调用新方法,计算三角形周长:
func main() {perim := Path{{1, 1},{5, 1},{5, 4},{1, 1},}fmt.Println(perim.Distance())
}
6.2 基于指针对象的方法
当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数是在太大我们希望能够避免进行这种默认的拷贝,该情况下我们就需要用到指针了:
func (p *Point) ScaleBy(factor float64) {p.X *= factorp.Y *= factor
}
上述方法的名字是(*Point).ScaleBy,括号是必须的,没有括号的话这个表达式可能会被理解为*(Point.ScaleBy),即类型方法的指针。
在现实的程序当中,一般会约定,如果 Point 这个类有一个指针作为接收器的方法,那么所有 Point 的方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数。
只有类型(比如 Point)和指向它们的指针(*Point),才可能出现在接收器声明里的两种接收器。此外,为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器当中的,比如:
type P *int
func (P) f() { /* ... ... ... */ } // Compile Error
想要调用指针类型方法(*Point).Salary,只需要提供一个 Point 类型的指针即可:
func main() {r := &Point{1, 2}r.ScaleBy(2) fmt.Println(*r) // {2, 4}
}
或者:
p := Point{1, 2}
(&p).ScaleBy(2)
不过上述两种方法稍显笨拙,Golang 本身在这种地方会帮助我们。如果接收器 p 是一个 Point 类型的变量,并且其方法需要一个 Point 指针作为接收器,可以用下面这种简短的写法:
p.ScaleBy(2)
编译器会隐式地帮助我们用&p去调用 ScaleBy 方法。这种简化写法只适用于“变量”,包括 struct 里的字段,比如p.X,以及 array 和 slice 内的元素。不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址无法获取:
Point{1, 2}.ScaleBy(2) // Compile Error
我们可以用一个*Point这样的接收器来调用 Point 的方法,因为我们可以通过地址找到这个变量,只要用解引用符号*获取变量即可。同样地,如果是一个指针想要调用其所指对象类型的非指针接收器方法时,Golang 的编译器会为我们隐式地插入解引用符:
pptr.Distance(q)
// 等价于
(*pptr).Distance(q)
6.2.1 nil 也是一个合法的接收器类型
就像一些函数允许 nil 指针作为参数一样,方法理论上也可以使用 nil 指针作为接收器,尤其当 nil 对于对象来说是合法的零值时,比如 map 或 slice。在下面的简单 int 链表的例子里,nil 代表的是空链表:
type IntList struct {Value intTail *IntList
}
func (list *IntList) Sum() int {if list == nil {return 0}return list.Value + list.Tail.Sum()
} // 使用递归的方法计算链表值的综合
不必担心此时 list 是否为 nil,就算 list 为 nil,它也可以调用 IntList 的方法。
6.3 通过嵌入结构体来扩展类型
下例定义了一个 ColoredPoint 类型:
package mainimport "image/color"type Point struct {X, Y float64
}type ColoredPoint struct {PointColor color.RGBA
}
我们完全可以将 ColoredPoint 定义为一个有三个字段的 struct,但是我们却将 Point 这个类型嵌入到了 ColoredPoint 来提供 X 和 Y 这两个字段。
结构当中内嵌其它结构可以使我们在定义 ColoredPoint 时得到一种句法上的简写形式,并使其包含 Point 类型所具有的一切字段,然后再定义一些自己的。我们可以直接认为通过嵌入的字段就是 ColoredPoint 自身的字段。
对于 Point 中的方法我们也有类似的用法,可以将 ColoredPoint 类型当作接收器来调用 Point 里的方法,即使 ColoredPoint 里没有声明这些方法:
func main() {red := color.RGBA{255, 0, 0, 255}blue := color.RGBA{0, 0, 255, 255}var p = ColoredPoint{Point{1, 1}, red}var q = ColoredPoint{Point{5, 4}, blue}fmt.Println(p.Distance(q.Point))p.ScaleBy(2)q.ScaleBy(2)fmt.Println(p.Distance(q.Point))
}
Point 类的方法也被引入到了 ColoredPoint 当中。一个 ColoredPoint 并不是一个 Point,但它 “has a Point”,并且它有从 Point 类里引入的 Distance 和 ScaleBy 方法。
在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前类型当中。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系:
type ColoredPoint struct {*PointColor color.RGBA
}func main() {red := color.RGBA{255, 0, 0, 255}blue := color.RGBA{0, 0, 255, 255}var p = ColoredPoint{&Point{1, 1}, red}var q = ColoredPoint{&Point{5, 4}, blue}fmt.Println(p.Distance(*q.Point))q.Point = p.Point // p 和 q 共享同一个 Pointp.ScaleBy(2)fmt.Println(*p.Point, *q.Point)
}
一个 struct 类型也可以有多个匿名字段,比如:
type ColoredPoint struct {Pointcolor.RGBA
}
6.4 方法值和方法表达式
执行一个方法常见的形式是p.Distance(),实际上这一步可以被拆分为两步来执行。p.Distance叫做“选择器”,选择器返回一个值,这个值是将方法Point.Distance绑定到特定接受其变量的函数。
这个函数可以不通过指定其接收器即可被调用,原因在于p.Distance这条语句已经将Distance方法与p绑定了。只需要传入函数的参数即可:
P := Point{1, 2}
q := Point{4, 6}distanceFromP := p.Distance
fmt.Println(distanceFromP(q))
var origin Point
fmt.Println(distanceFromP(origin))
在一个包的API需要一个函数值,且调用方法希望操作的是某一个绑定了对象的方法的话,方法“值”会非常有用。例如,下例中的time.AfterFunc这个函数的功能是在指定的延迟时间之后来执行一个函数。且这个函数操作的是一个Rocket 对象 r:
type Rocket struct { /* … … … */ }
func (r *Rocket) Launch() { /* … … … */ }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })
直接将方法的值传入 AfterFunc 的话可以更简短:
time.AfterFunc(10 * time.Second, r.Launch)
与调用一个普通的函数相比,调用一个方法时,必须要用选择器语法来指定方法的接收器。当 T 是一个类型时,方法表达式可能会写作T.f或(*T).f,会返回一个函数“值”,这种函数会将其第一个参数用作接收器,所以可以用类似于函数调用的方式来对方法表达式进行调用(意思是,直接将类型.方法与某个变量绑定,此时这个变量类似于一个函数,加入原先的方法有一个参数,此时的函数有两个参数,第一个参数将会被视为选择器参数将第一个传入该函数的变量与方法绑定):
P := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance // method expression
fmt.Println(distance(p, q)) // “5”
fmt.Printf(“%T\n”, distance)// “func(Point, Point) float64”scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p) // “{2, 4}”
fmt.Printf(“%T\n”, scale) // “func(*Point, float64)”
下例是一个更复杂的例子:
type Point struct{ X, Y float64 }func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }type Path []Pointfunc (path Path) TranslateBy(offset Point, add bool) {var op func(p, q Point) Pointif add {op = Point.Add} else {op = Point.Sub}for i := range path {path[i] = op(path[i], offset)}
}
6.6 封装
“封装”指的是一个对象的变量或方法对调用方是不可见的,是 OOP 最关键的一个概念。
Golang 只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于 struct 或一个类型的方法。因而如果我们想要封装一个对象,必须将其定义为一个 struct。
一个例子如下:
type IntSet struct {words []uint64
}
当然我们也可以直接将 IntSet 定义为 slice 类型,这样我们就需要把代码中所有方法里的s.word用*s替换掉:
type IntSet []uint64
上述这种基于名字的手段使得在 Golang 中最小的封装单元是 package,而不是像其他语言一样的类型。一个 struct 类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。
封装最重要的优点是阻止了外部调用方对对象内部的值进行修改:
type Counter struct { n int }
func (c *Counter) N() int { return c.n }
func (c *Counter) Increment() { c.n++ }
func (c *Counter) Reset() { c.n = 0 }
只用来访问或修改内部变量的函数被称为 setter 或 getter。
注意:Golang 的编码风格不禁止直接导出字段。当然,一旦进行了导出,就没有办法在保证 API 兼容的情况下去除对其的导出,所以在一开始的选择一定要经过深思熟虑并且要考虑到包内部的一些不变量的保证,未来可能的变化,以及调用方的代码质量是否会因为包的一点修改而变差。
相关文章:
【Go语言圣经】第六节:方法
第六章:方法 6.1 方法声明 在函数声明时,在其名字之前放上一个变量,这就是声明了变量对应类型的一个方法,相当于为这种类型定义了一个独占的方法。 下例为 Point 类型声明了计算两个点之间距离的方法: package mai…...
【Leetcode刷题记录】45. 跳跃游戏 II--贪心算法
45. 跳跃游戏 II 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i j] 处: 0 < j < nums[i]i j < n 返回到达 num…...
mysql_init和mysql_real_connect的形象化认识
解析总结 1. mysql_init 的作用 mysql_init 用于初始化一个 MYSQL 结构体,为后续数据库连接和操作做准备。该结构体存储连接配置及状态信息,是 MySQL C API 的核心句柄。 示例: MYSQL *conn mysql_init(NULL); // 初始化连接句柄2. mysql_…...
Qt网络相关
“ 所有生而孤独的人,葆有的天真 ” 为了⽀持跨平台, QT对⽹络编程的 API 也进⾏了重新封装。本章会上手一套基于QT的网络通信编写。 UDP Socket 在使用Qt进行网络编程前,需要在Qt项目中的.pro文件里添加对应的网络模块( network ). QT core gui net…...
deepseek接入pycharm 进行AI编程
要将DeepSeek接入PyCharm进行AI编程,可以按照以下步骤操作: ### 1. 获取DeepSeek API访问权限 DeepSeek通常以API的形式对外提供服务,你需要在其官方网站注册账号,申请API访问权限。在申请通过后,会获得API密钥(API Key),这是后续调用API的关键凭证。 ### 2. 安装必要…...
Verilog基础(三):过程
过程(Procedures) - Always块 – 组合逻辑 (Always blocks – Combinational) 由于数字电路是由电线相连的逻辑门组成的,所以任何电路都可以表示为模块和赋值语句的某种组合. 然而,有时这不是描述电路最方便的方法. 两种always block是十分有用的&am…...
生成式AI安全最佳实践 - 抵御OWASP Top 10攻击 (上)
今天小李哥将开启全新的技术分享系列,为大家介绍生成式AI的安全解决方案设计方法和最佳实践。近年来,生成式 AI 安全市场正迅速发展。据 IDC 预测,到 2025 年全球 AI 安全解决方案市场规模将突破 200 亿美元,年复合增长率超过 30%…...
.Net WebAPI -[HttpPut(“{fileServiceId:int}“)]
[HttpPut("{fileServiceId:int}")] 这个写法是 ASP.NET Core 中的一个路由特性,用于定义一个 HTTP PUT 请求的路由,并指定路由参数的类型。 解析 HttpPut [HttpPut]: 这是一个 ASP.NET Core 的路由特性,用于标记一个方…...
[EAI-027] RDT-1B,目前最大的用于机器人双臂操作的机器人基础模型
Paper Card 论文标题:RDT-1B: a Diffusion Foundation Model for Bimanual Manipulation 论文作者:Songming Liu, Lingxuan Wu, Bangguo Li, Hengkai Tan, Huayu Chen, Zhengyi Wang, Ke Xu, Hang Su, Jun Zhu 论文链接:https://arxiv.org/ab…...
C基础寒假练习(7)
一、有 1、2、3、4个数字,能组成多少互不相同且无重复的三位? 都是多少? #include <stdio.h> int main() {// 定义数字数组int digits[] {1, 2, 3, 4};int n sizeof(digits) / sizeof(digits[0]);// 嵌套循环遍历所有排列for (int …...
Ajax:重塑Web交互体验的人性化探索
在数字化时代,网页的交互性和响应速度已成为衡量用户体验的关键指标。Ajax(Asynchronous JavaScript and XML),作为前端与后端沟通的桥梁,凭借其异步通信的能力,极大地提升了网页的动态性和用户友好度&…...
【DeepSeek背后的技术】系列二:大模型知识蒸馏(Knowledge Distillation)
目录 1 引言2 操作步骤和公式说明2.1 准备教师模型(Teacher Model)和学生模型(Student Model)2.2 生成软标签(Soft Labels)2.3 定义蒸馏损失函数2.4 训练学生模型2.5 调整超参数2.6 评估与部署 3 其他知识蒸…...
【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.14 内存映射:处理超大型数组的终极方案
2.14 内存映射:处理超大型数组的终极方案 目录 #mermaid-svg-G91Kn9O4eN2k8xEo {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-G91Kn9O4eN2k8xEo .error-icon{fill:#552222;}#mermaid-svg-G91Kn9O4eN2k…...
【C++】STL——vector的使用
目录 💕1.vector介绍 💕2.vector的基本用法 💕3.vector功能的具体用法 (讲解) 💕4.vector——size,capacity函数的使用 (简单略讲) 💕5.resizeÿ…...
springboot/ssm互联网智慧医院体检平台web健康体检管理系统Java代码编写
springboot/ssm互联网智慧医院体检平台web健康体检管理系统Java代码编写 基于springboot(可改ssm)vue项目 开发语言:Java 框架:springboot/可改ssm vue JDK版本:JDK1.8(或11) 服务器:tomcat 数据库&am…...
介绍一下Mybatis的Executor执行器
Executor执行器是用来执行我们的具体的SQL操作的 有三种基本的Executor执行器: SimpleExecutor简单执行器 每执行一次update或select,就创建一个Statement对象,用完立刻关闭Statement对象 ReuseExecutor可重用执行器 可重复利用Statement…...
Wide Deep 模型:记忆能力与泛化能力
实验和完整代码 完整代码实现和jupyter运行:https://github.com/Myolive-Lin/RecSys--deep-learning-recommendation-system/tree/main 引言 Wide & Deep 模型是一种结合了线性模型(Wide)和深度神经网络(Deep)的混…...
Hot100之矩阵
73矩阵置零 题目 思路解析 收集0位置所在的行和列 然后该行全部初始化为0 该列全部初始化为0 代码 class Solution {public void setZeroes(int[][] matrix) {int m matrix.length;int n matrix[0].length;List<Integer> list1 new ArrayList<>();List<…...
Python语言的安全开发
Python语言的安全开发 引言 在信息技术迅速发展的今天,网络安全问题愈发凸显。随着Python语言的广泛应用,尤其是在数据分析、人工智能、Web开发等领域,其安全问题越来越受到重视。Python作为一门高效且易于学习的编程语言,虽然在…...
蓝桥杯刷题DAY3:Horner 法则 前缀和+差分数组 贪心
所谓刷题,最重要的就是细心 📌 题目描述 在 X 进制 中,每一数位的进制不固定。例如: 最低位 采用 2 进制,第二位 采用 10 进制,第三位 采用 8 进制, 则 X 进制数 321 的十进制值为ÿ…...
基于距离变化能量开销动态调整的WSN低功耗拓扑控制开销算法matlab仿真
目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.算法仿真参数 5.算法理论概述 6.参考文献 7.完整程序 1.程序功能描述 通过动态调整节点通信的能量开销,平衡网络负载,延长WSN生命周期。具体通过建立基于距离的能量消耗模型&am…...
vscode(仍待补充)
写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh? debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...
【论文阅读28】-CNN-BiLSTM-Attention-(2024)
本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...
pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...
Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...
Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)
在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马(服务器方面的)的原理,连接,以及各种木马及连接工具的分享 文件木马:https://w…...
服务器--宝塔命令
一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行! sudo su - 1. CentOS 系统: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...
2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...
GO协程(Goroutine)问题总结
在使用Go语言来编写代码时,遇到的一些问题总结一下 [参考文档]:https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现: 今天在看到这个教程的时候,在自己的电…...
