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

【Go】P11 掌握 Go 语言函数(二):进阶玩转高阶函数、闭包与 Defer/Panic/Recover

目录

  • 高阶函数
    • 函数作为参数
    • 函数作为返回值
  • 匿名函数
    • 匿名自执行函数 (IIFE)
  • 函数的闭包
  • 递归函数
  • defer、panic 与 recover
    • defer 语句
    • panic 与 recover
    • errors 包与 errors.New()
  • 总结

在这里插入图片描述

在Go语言中,函数不仅仅是代码的执行单元,它们本身也是一种。你可以将它们赋值给变量,作为参数传递给其他函数,甚至作为另一个函数的返回值。这种特性为Go语言带来了强大的灵活性。

本文将带你深入探索Go语言中函数的高阶用法,包括高阶函数、匿名函数、闭包、递归,以及与之密切相关的 deferpanicrecovererrors 包。


高阶函数

Go语言高阶函数,是指那些可以接受其他函数作为参数,或者将函数作为返回值的函数。

函数作为参数

函数作为参数传递,最常见的应用场景就是回调函数或策略模式。这允许我们编写更通用、更灵活的代码。

示例: 假设我们有一个计算函数,它接受两个整数和一个“操作”函数。

package mainimport "fmt"// 定义一个函数类型,它接受两个int参数并返回一个int
type operation func(int, int) int// add 和 subtract 都是 operation 类型的函数
func add(a, b int) int {return a + b
}func subtract(a, b int) int {return a - b
}// calculate 是一个高阶函数,它接受一个 operation 类型的函数作为参数
func calculate(a, b int, op operation) int {result := op(a, b)fmt.Printf("计算结果: %d\n", result)return result
}func main() {calculate(10, 5, add)      // 输出: 计算结果: 15calculate(10, 5, subtract) // 输出: 计算结果: 5
}

函数作为返回值

函数也可以作为另一个函数的返回值。这在创建“工厂”函数或实现闭包时非常有用。

示例: 创建一个“乘法器”工厂。

package mainimport "fmt"// createMultiplier 返回一个新的函数
// 这个新函数会将其参数乘以 factor
func createMultiplier(factor int) func(int) int {// 返回一个匿名函数return func(x int) int {return x * factor}
}func main() {// 创建一个“乘以2”的函数double := createMultiplier(2)// 创建一个“乘以3”的函数triple := createMultiplier(3)fmt.Println("5 乘以 2 =", double(5)) // 输出: 5 乘以 2 = 10fmt.Println("5 乘以 3 =", triple(5)) // 输出: 5 乘以 3 = 15
}

这个例子引出了我们下一个重要概念:闭包


匿名函数

匿名函数,顾名思义,就是没有名字的函数。它们在需要一个临时、短小的函数时非常有用,尤其是在高阶函数和 go 协程中。

func main() {// 将匿名函数赋值给变量greet := func(name string) {fmt.Println("Hello,", name)}greet("Go") // 输出: Hello, Go
}

匿名自执行函数 (IIFE)

匿名函数也可以在定义后立即执行,这被称为 IIFE (Immediately Invoked Function Expression)

func main() {func(message string) {fmt.Println(message)}("我是一个立即执行的匿名函数!") // 输出: 我是一个立即执行的匿名函数!
}

仔细观察,其实匿名执行函数与函数的本质区别是在函数体后直接增加 () 并按照函数设定的形参填充内容。


函数的闭包

闭包可以理解为“定义在一个函数内部的函数”。

更准确地说,闭包是一个函数值,它引用了其函数体之外的变量。这个函数可以访问并修改那些被引用的变量。

闭包的作用是什么?

闭包最大的价值在于它能将函数内部和函数外部连接起来。它允许一个变量“常驻内存”(就像全局变量一样),但又不会污染全局命名空间(保持了局部变量的私有性)。

示例: 让我们实现一个计数器,它就利用了闭包的特性。

package mainimport "fmt"// incrementor 返回一个函数
// 这个返回的函数“关闭”了变量 i
func incrementor() func() int {i := 0 // i 是自由变量,被闭包引用// 返回的这个匿名函数就是闭包return func() int {i++ // 每次调用时,修改的是同一个 ireturn i}
}func main() {// counter1 和 counter2 是两个独立的闭包实例// 它们各自拥有自己的 icounter1 := incrementor()fmt.Println(counter1()) // 输出: 1fmt.Println(counter1()) // 输出: 2fmt.Println(counter1()) // 输出: 3fmt.Println("---")counter2 := incrementor()fmt.Println(counter2()) // 输出: 1
}

在上面的例子中,incrementor 每被调用一次,就会创建一个新的 i 变量。返回的匿名函数“记住”了它被创建时的环境(即那个特定的 i)。因此 counter1 counter2 互不干扰。


递归函数

递归函数是指在函数体内调用其自身的函数。在 Go 中,任何函数都可以调用其他函数,当然也包括它自己。

使用递归时,必须定义一个明确的“基本情况”(Base Case)或退出条件,否则函数将无限调用下去,直到耗尽栈空间导致 stack overflow

示例: 计算阶乘。

package mainimport "fmt"func factorial(n int) int {// 基本情况:0 的阶乘是 1if n == 0 {return 1}// 递归调用return n * factorial(n-1)
}func main() {fmt.Println("5! =", factorial(5)) // 输出: 5! = 120
}

defer、panic 与 recover

这三个关键字共同构成了Go语言的错误处理和程序健壮性机制。

defer 语句

defer 语句会将其后面跟随的函数调用延迟到其所在的函数即将返回时执行。

关键特性:

  • LIFO(后进先出): 如果一个函数中有多个 defer 语句,它们会像一样,按注册的逆序执行。最后注册的 defer 最先执行。
  • 参数预计算: defer 注册时,它后面函数的参数(包括接收者)会被 立即 求值。

示例分析: 让我们来分析一下你提供的这个经典例子:

package mainimport "fmt"func calc(index string, a, b int) int {ret := a + bfmt.Println(index, a, b, ret)return ret
}func main() {x := 1y := 2defer calc("AA", x, calc("A", x, y))x = 10defer calc("BB", x, calc("B", x, y))y = 20
}

执行过程逐步分解:

  1. x 初始化为 1y 初始化为 2
  2. 遇到第一个 defer:defer calc("AA", x, calc("A", x, y))
    • defer 语句的参数必须 立即 求值。
    • x 此时是 1。
    • 第三个参数 calc("A", x, y) 必须 立即执行 以获取其返回值。
    • 执行 calc("A", 1, 2)
    • 打印:A 1 2 3
    • calc("A", 1, 2) 返回 3
    • 现在,第一个 defer 语句被注册为 defer calc("AA", 1, 3)。它被压入 defer 栈。
  3. x 被赋值为 10
  4. 遇到第二个 defer:defer calc("BB", x, calc("B", x, y))
    • 参数 立即 求值。
    • x 此时是 10
    • y 此时是 2
    • 第三个参数 calc("B", x, y) 必须 立即执行
    • 执行 calc("B", 10, 2)
    • 打印:B 10 2 12
    • calc("B", 10, 2) 返回 12
    • 第二个 defer 语句被注册为 defer calc("BB", 10, 12)。它被压入 defer 栈(位于 “AA” 之上)。
  5. y 被赋值为 20。(这个赋值对已经注册的 defer 没有影响)
  6. main 函数即将返回。开始执行 defer 栈(LIFO)。
  7. 执行栈顶的 defer:calc("BB", 10, 12)
    • 打印:BB 10 12 22
  8. 执行下一个 defer:calc("AA", 1, 3)
    • 打印:AA 1 3 4

最终输出:

A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4

defer 与命名返回值: defer 语句可以读取和修改函数的 命名返回值

func deferredReturn() (result int) { // 'result' 是命名返回值defer func() {result = result * 2 // 在函数返回前,修改 result}()return 5 // 1. 赋值 result = 5; 2. 执行 defer; 3. 返回
}func main() {fmt.Println(deferredReturn()) // 输出: 10
}

带着对 defer 的感觉,我们来面会剩下的两个朋友 panicrecover

panic 与 recover

Go语言没有传统的 try-catch 异常机制,而是使用 panicrecover 来处理运行时发生的严重错误。

  • panic 是一个内置函数,用于引发一个运行时“恐慌”。它会立即停止当前函数的正常执行,然后开始 解开(unwinding) Goroutine 的调用栈,并执行该过程中遇到的所有 defer 语句。
  • recover 是一个内置函数,用于重新获得对恐慌的 Goroutine 的控制权。recover 只有在 defer 函数中调用时才有效。

panicrecover 结合使用,可以防止程序因意外错误而崩溃,常用于库或框架中以捕获下游代码的恐慌。

package mainimport "fmt"func safeDivide(a, b int) int {// 使用 defer 和 recover 来捕获可能的 panicdefer func() {// recover() 只有在 defer 中才有效if r := recover(); r != nil {fmt.Println("捕获到 panic:", r)// 可以在这里设置默认返回值,但此示例中 int 默认为 0}}()if b == 0 {// 引发一个 panicpanic("除数不能为零!")}return a / b
}func main() {fmt.Println("开始执行...")result1 := safeDivide(10, 2)fmt.Println("10 / 2 =", result1)result2 := safeDivide(10, 0) // 这将引发 panic,但会被 recoverfmt.Println("10 / 0 =", result2)fmt.Println("程序继续执行...") // 因为 panic 被恢复了
}

输出:

开始执行...
10 / 2 = 5
捕获到 panic: 除数不能为零!
10 / 0 = 0
程序继续执行...

errors 包与 errors.New()

对于可预见的、非灾难性的错误(例如“文件未找到”、“用户输入无效”),Go语言的惯例是使用 error 类型作为函数的最后一个返回值。

errors 包提供了一个非常简单的函数 New(),用于创建一个包含给定错误信息的 error 值。

package mainimport ("errors""fmt"
)// 遵循 Go 的惯例,error 作为最后一个返回值
func divide(a, b int) (int, error) {if b == 0 {// 创建一个新的 errorreturn 0, errors.New("division by zero")}// 成功时,error 返回 nilreturn a / b, nil
}func main() {result, err := divide(10, 2)if err != nil {fmt.Println("发生错误:", err)} else {fmt.Println("结果:", result)}result, err = divide(10, 0)if err != nil {fmt.Println("发生错误:", err)} else {fmt.Println("结果:", result)}
}

总结

Go语言的函数远不止是代码块。它们是强大的数据类型,通过高阶函数、匿名函数和闭包,我们可以编写出高度灵活、解耦且易于维护的代码。同时,deferpanicrecover 机制,结合 error 接口,为我们提供了构建健壮、可靠程序的完整工具集。掌握这些概念,是从Go新手迈向资深开发者的关键一步。


2025.10.22 西三旗

http://www.dtcms.com/a/516615.html

相关文章:

  • 无奈!我用go写了个MySQL服务
  • 重庆网站建设业务招聘网站推广方式主要通过
  • GaussDB 数据集成方案:ETL 工具如何简化企业上云过程
  • 如何解决 pip install 安装报错 externally-managed-environment(PEP 668)问题
  • 相向双指针|两数之和II-输入有序数组|三数之和|统计和小于目标的下标对数目|最接近的三数之和|四数之和|有效三角形的个数
  • ffmpeg4.4.2 gcc 15.2.0 编译错误
  • 免费的大语言模型API接口
  • css3 学习笔记
  • 高水平的网站建设南昌做个网站多少钱
  • 宁夏建设工程质量安全监督总网站wordpress发送文章链接过期
  • Vscode 如何配置远程环境的 ssh 连接
  • 昆明参差网站开公司建网站
  • 专业点的网站制作公司建设一个公司网站需要什么条件
  • Mybatis-Spring重要组件介绍
  • 【ROS2】行为树 BehaviorTree(十):行为树节点注册、动态加载过程详解
  • MapperMethod中的SqlCommand和MethodSignature
  • 代码随想录Day55|108. 冗余连接、109. 冗余连接II
  • 最简单的方式做最系统的教学【计算机组成入门到入土】计组核心:一篇文章搞定指令格式与所有寻址方式
  • 30.redis消息队列
  • 如何做体育彩票网站什么是网络营销方案
  • 济南网站优化公司排名四川省建设工程交易中心网站
  • 企业安全防护之——防火墙
  • 电商网站制作公司网站单个页面紧张搜索引擎蜘蛛
  • mac 安装 dmg 格式程序,打开显示已损坏问题
  • CentOS 7 系统安装教程
  • 网站设计制作从哪里学起精准营销定义
  • Python 第二十一节 基础案例练习
  • 视频分析软件识别人像特征
  • Qt:Qt下载慢的解决方案
  • 南昌做公司网站哪家好做一款app需要多少钱费用