【golang】函数(func)正确使用姿势

news2024/11/13 14:26:49

函数不但可以用于封装代码、分割功能、解耦逻辑,还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等等,就像切片和字典的值那样。

而更深层次的含义就是:函数值可以由此成为能够被随意传播的独立逻辑组件(或者说功能模块)。

package main
import "fmt"
    type Printer func(contents string) (n int, err error)
func printToStd(contents string) (bytesNum int, err error) {
    return fmt.Println(contents)
}
func main() {
    var p Printer
    p = printToStd
    p("something")
}

在func右边的就是这个函数类型的参数列表和结果列表。其中,参数列表必须由圆括号包裹,而只要结果列表中只有一个结果声明,并且没有为它命名,我们就可以省略掉外围的圆括号。

书写函数签名的方式与函数声明的是一致的。只是紧挨在参数列表左边的不是函数名称,而是关键字func。这里函数名称和func互换了一下位置而已。

函数的签名其实就是函数的参数列表和结果列表的统称,它定义了可用来鉴别不同函数的那些特征,同时也定义了我们与函数交互的方式。

注意,各个参数和结果的名称不能算作函数签名的一部分,甚至对于结果声明来说,没有名称都可以。

只要两个函数的参数列表和结果列表中的元素顺序及其类型是一致的,我们就可以说它们是一样的函数,或者说是实现了同一个函数类型的函数。

严格来说,函数的名称也不能算作函数签名的一部分,它只是我们在调用函数时,需要给定的标识符而已。

我在上面声明的函数printToStd的签名与Printer的是一致的,因此前者是后者的一个实现,即使它们的名称以及有的结果名称是不同的。

通过main函数中的代码,我们就可以证实这两者的关系了,我顺利地把printToStd函数赋给了Printer类型的变量p,并且成功地调用了它。

怎样编写高阶函数?

高阶函数可以满足下面的两个条件:

1.接受其他的函数作为参数传入

2.把其他的函数作为结果返回

具体的问题是,我想通过编写calculate函数来实现两个整数间的加减乘除运算,但是希望两个整数和具体的操作都由该函数的调用方给出,那么,这样一个函数应该怎样编写呢?

典型回答

首先,我们来声明一个名叫operate的函数类型,它有两个参数和一个结果,都是int类型的。

type operate func(x,y int) int

然后,我们编写calculate函数的签名部分。这个函数除了需要两个int类型的参数之外,还应该有一个operate类型的参数。

该函数的结果应该有两个,一个是int类型的,代表真正的操作结果,另一个应该是error类型的,因为如果那个operate类型的参数值为nil,那么就应该直接返回一个错误。

函数类型属于引用类型,它的值可以为nil,而这种类型的零值恰恰就是nil。

func calculate(x int, y int, op operate) (int, error) {
    if op == nil {
        return 0, errors.New("invalid operation")
    }
    return op(x, y), nil
}

calculate函数实现起来就很简单了。我们需要先用卫述语句检查一下参数,如果operate类型的参数opnil,那么就直接返回0和一个代表了具体错误的error类型值。

卫述语句是指被用来检查关键的先决条件的合法性,并在检查未通过的情况下立即终止当前代码块执行的语句。在 Go 语言中,if 语句常被作为卫述语句。

如果检查无误,那么就调用op并把那两个操作数传给它,最后返回op返回的结果和代表没有错误发生的nil。

op := func(x, y int) int {
    return x + y
}

calculate函数就是一个高阶函数。但是我们说高阶函数的特点有两个,而该函数只展示了其中一个特点,即:接受其他的函数作为参数传入

另一个特点,把其他的函数作为结果返回

type operate func(x, y int) int

type calculateFunc func(x int, y int) (int, error)

func genCalculator(op operate) calculateFunc {
	return func(x int, y int) (int, error) {
		if op == nil {
			return 0, errors.New("invalid operation")
		}
		return op(x, y), nil
	}
}

func main() {
	x, y = 56, 78
	add := genCalculator(op)
	result, err = add(x, y)
	fmt.Printf("The result: %d (error: %v)\n",
		result, err)
}

如何实现闭包?

闭包又是什么?你可以想象一下,在一个函数中存在对外来标识符的引用。所谓的外来标识符,既不代表当前函数的任何参数或结果,也不是函数内部声明的,它是直接从外边拿过来的。

还有个专门的术语称呼它,叫自由变量,可见它代表的肯定是个变量。实际上,如果它是个常量,那也就形成不了闭包了,因为常量是不可变的程序实体,而闭包体现的却是由“不确定”变为“确定”的一个过程。

我们说的这个函数(以下简称闭包函数)就是因为引用了自由变量,而呈现出了一种“不确定”的状态,也叫“开放”状态。

也就是说,它的内部逻辑并不是完整的,有一部分逻辑需要这个自由变量参与完成,而后者到底代表了什么在闭包函数被定义的时候却是未知的。

即使对于像Go语言这种静态类型的编程语言而言,我们在定义闭包函数的时候最多也只能知道自由变量的类型。

刚刚提到的genCalculator函数内部,实际上就实现了一个闭包,而genCalculator函数也是一个高阶函数。

func genCalculator(op operate) calculateFunc {
	return func(x int, y int) (int, error) {
		if op == nil {
			return 0, errors.New("invalid operation")
		}
		return op(x, y), nil
	}
}

上面匿名的函数就是一个闭包函数。它里面使用的变量op既不代表它的任何参数或结果也不是它自己声明的,而是定义它的genCalculator函数的参数,所以是一个自由变量。

这个自由变量究竟代表了什么,这一点并不是在定义这个闭包函数的时候确定的,而是在genCalculator函数被调用的时候确定的。

只有给定了该函数的参数op,我们才能知道它返回给我们的闭包函数可以用于什么运算。看到if op == nil {那一行了吗?Go 语言编译器读到这里时会试图去寻找op所代表的东西,它会发现op代表的是genCalculator函数的参数,然后,它会把这两者联系起来。这时可以说,自由变量op被“捕获”了。

当程序运行到这里的时候,op就是那个参数值了。如此一来,这个闭包函数的状态就由“不确定”变为了“确定”,或者说转到了“闭合”状态,至此也就真正地形成了一个闭包。

image.png

实现闭包的意义在哪里呢?

表面上看,我们只是延迟实现了一部分程序逻辑或功能而已,但实际上,我们是在动态地生成那部分程序逻辑。

我们可以借此在程序运行的过程中,根据需要生成功能不同的函数,继而影响后续的程序行为。这与GoF设计模式中的“模板方法”模式有着异曲同工之妙。

传入函数的那些参数值后来怎么样了?

package main

import "fmt"

func main() {
    array1 := [3]string{"a", "b", "c"}
    fmt.Printf("The array: %v\n", array1)
    array2 := modifyArray(array1)
    fmt.Printf("The modified array: %v\n", array2)
    fmt.Printf("The original array: %v\n", array1)
}
func modifyArray(a [3]string) [3]string {
    a[1] = "x"
    return a
}

这个命令源码文件在运行之后会输出什么?

答案:原数组不会改变。为什么呢?原因是,所有传给函数的参数值都会被复制,函数在其内部使用的并不是参数值的原值,而是它的副本。

由于数组是值类型,所以每一次复制都会拷贝它,以及它的所有元素值。在modify函数中修改的只是原数组的副本而已,并不会对原数组造成任何影响。

注意,对于引用类型,比如:切片、字典、通道,像上面那样复制它们的值,只会拷贝它们本身而已,并不会拷贝它们引用的底层数据。也就是说,这时只是浅表复制,而不是深层复制。

以切片值为例,如此复制的时候,只是拷贝了它指向底层数组中某一个元素的指针,以及它的长度值和容量值,而它的底层数组并不会被拷贝。

另外还要注意,就算我们传入函数的是一个值类型的参数值,但如果这个参数值中的某个元素是引用类型的,那么我们仍然要小心

complexArray1 := [3][]string{
    []string{"d", "e", "f"},
    []string{"g", "h", "i"},
    []string{"j", "k", "l"},
}

变量complexArray1是[3][]string类型的,也就是说,虽然它是一个数组,但是其中的每个元素又都是一个切片。这样一个值被传入函数的话,函数中对该参数值的修改会影响到complexArray1本身吗?

若是修改数组中的切片的某个元素,会影响原数组。若是修改数组的某个元素即a[1]=[]string{“x”}就不会影响原数组。谨记Go中都是浅拷贝,值类型和引用类型的区别

文章学习自郝林老师的《Go语言36讲》

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

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

相关文章

使用PyMuPDF库的PDF合并和分拆程序

PDF工具应用程序是一个使用wxPython和PyMuPDF库编写的简单工具,用于合并和分拆PDF文件。它提供了一个用户友好的图形界面,允许用户选择源文件夹和目标文件夹,并对PDF文件进行操作。 C:\pythoncode\blog\pdfmergandsplit.py 功能特点 选择文…

高速数据采集卡---AD采集FMC子卡模块产品资料下载

FMC152是一款基于VITA57.1标准的,实现2路14-bit、2GSPS/2.6GSPS/3GSPS AD采集FMC子卡模块。该模块可直接与FPGA载卡配合使用,板卡ADC器件采用ADI公司的AD9208芯片,与ADI公司的AD9689可以实现PIN脚兼容。该模块全功率模拟输入带宽(…

【FAQ】安防监控视频云存储平台EasyNVR频繁离线的原因排查与解决

有用户反馈,在使用EasyNVR时会出现通道频繁离线的情况。针对该反馈我们立即进行了排查。 安防视频监控汇聚EasyNVR视频集中存储平台,是基于RTSP/Onvif协议的安防视频平台,可支持将接入的视频流进行全平台、全终端分发,分发的视频流…

(二)结构型模式:6、外观模式(Facade Pattern)(C++实例)

目录 1、外观模式(Facade Pattern)含义 2、外观模式的UML图学习 3、外观模式的应用场景 4、外观模式的优缺点 5、C实现外观模式的简单实例 1、外观模式(Facade Pattern)含义 外观模式(Facade Pattern)…

气象监测设备——分类与应用

气象监测设备多种多样,不同的应用场景选择适合的气象监测设备才能事半功倍。 在植保站中,可以 选择农林植保小气候气象站,它可以帮助植保站的工作人员完成气象监测工作,对天气环境进行预报预测,为植物的健康生长提供保…

Windows系统如何查看端口被占用程序和停止占用端口程序

windows系统如何查看端口被占用程序和停止占用端口程序,以及windows常用的网络命令详解 打开命令窗口 电脑右下方,搜索框,输入“cmd”,回车打开dos命令窗口 查看系统所有被占用的端口命令 netstat -ano 查看指定端口是否被占用命令 netst…

CS144 计算机网络 Lab1:Stream Reassembler

前言 上一篇博客中我们完成了 Lab0,使用双端队列实现了一个字节流类 ByteStream,可以向字节流中写入数据并按写入顺序读出数据。由于网络环境的变化,发送端滑动窗口内的数据包到达接收端时可能失序,所以接收端收到数据之后不能直…

Windows Server --- RDP远程桌面服务器激活和RD授权

RDP远程桌面服务器激活和RD授权 一、激活服务器二、设置RD授权 系统:Window server 2008 R2 服务:远程桌面服务 注:该方法适合该远程桌面服务器没网络状态下(离线),激活服务器。 一、激活服务器 1.打开远…

Spring学习笔记之Bean的循环依赖问题

文章目录 什么是Bean的循环依赖singleton下的set注入产生的循环依赖prototype下的set注入产生的循环依赖singleton下的构造注入产生的循环依赖Spring解决循环循环的机理(面试题) 什么是Bean的循环依赖 A对象中有B属性。B对象中有A属性。这就是循环依赖。…

leetcode 1614.括号的最大嵌套深度

⭐️ 题目描述 🌟leetcode链接:括号的最大嵌套深度 ps: 使用数据结构栈来存储 ( 在使用 maxDepth 变量记录栈顶 top 的最大值,当遇到 ) 时删除栈顶元素。举个例子 (1)((2))(((3))),当遇到第一个 ( 时 top 1&#xff…

对dubbo的DubboReference.check的参数进行剖析

背景 在使用dubbo的时候,发现当消费者启动的时候,如果提供者没有启动,即使提供者后来启动了,消费者也调不通提供者提供的接口了。 注册中心使用都是nacos dubbo版本是3.0.4 例子 接口 public interface DemoService {String…

中期国际:外汇交易的利器:善用挂单技巧优化交易策略

在外汇交易中,挂单技巧是提高交易效率和灵活性的重要利器之一。善用限价单和止损单可以帮助交易者有效规避风险、控制入场点和出场点,从而提高交易效果。本文将介绍一些MT4挂单技巧,以帮助交易者优化交易策略,提高交易效率。 1. 了…

猿辅导设立“青少年科学探索基金”,鼓励天才少年投入科学研究

“少年智则国智,少年富则国富,少年强则国强。”国家发展离不开人才的培养。伴随我国进入高质量发展轨道,科学、人才、教育三位一体融合发展已经刻不容缓。我国基础学科人才紧缺成了不争的事实。目前,中国的GDP目前已是世界第二位&…

nginx创建和监听套接字分析

https://cloud.tencent.com/developer/article/1859856 简介 nginx作为一个web服务器,肯定是有listen套接字对外提供服务的,listen套接字是用于接收HTTP请求。 nginx监听套接字的创建是根据配置文件的内容来创建的,在nginx.conf文件中有…

视频音乐如何转换成mp3?教你超简单的转换方法

MP3文件通常比视频文件更小。因此,通过将音乐从视频中提取并转换为MP3格式,您可以更轻松地存储和传输它们。如果计划在手机或其他设备上存储音乐,转换为MP3格式可以帮助我们节省存储空间。而且,如果需要将音乐发送给朋友或上传到互…

基于JAVA高校校园点餐系统-lw+ppt

文章目录 前言一、主要技术javaMysql数据库JSP技术 二、系统设计1. 系统结构图 三、功能截图总结 前言 21世纪的今天,随着社会的发展与进步,人们对信息科学的认识已从低层次提升到高层次,从感性认识逐渐转变为理性认识。管理工作的重要性也逐…

新生录取查询系统怎么制作?

在制作新生录取查询系统前,先跟老师们介绍一下招生录取的详细流程,以便老师们更好的完成录取工作的筹备,顺利过渡招生季! 1. 招生宣传和报名:学校通过各种途径进行招生宣传,向学生和家长介绍学校的特色、教…

图数据库_Neo4j学习cypher语言_常用函数_关系函数_字符串函数_聚合函数_数据库备份_数据库恢复---Neo4j图数据库工作笔记0008

然后再来看一些常用函数,和字符串函数,这里举个例子,然后其他的 类似 可以看到substring字符串截取函数 可以看到截取成功 聚合函数 这里用了一个count(n) 统计函数,可以看到效果 关系函数,我们用过就是id(r) 可以取出对应的r的id来这样..

北京影视展BIRTV 2023亮点提前盘点

2023年8月23-26日,“融合创新 面向未来”——由国家广播电视总局和中央广播电视总台共同指导,中国广播电视国际经济技术合作总公司主办的第三十届北京国际广播电影电视展览会(BIRTV2023)将在北京中国国际展览中心(朝阳…

大股东被纪检监察调查,会否成为大牧人上市之路的又一拦路虎?

据悉,深圳证券交易所上市审核委员会已经定于2023年8月17日召开2023年第63次上市审核委员会审议会议,审核青岛大牧人机械股份有限公司(即“大牧人”)首发上市。这已经是大牧人因股东股权纷争(见相关媒体报道&#xff09…