Go 编程实战:匿名函数 Anonymous Function——没有名字的函数与灵活回调

上一篇我们学习了 Go 中的函数,掌握了参数、返回值、多返回值、可变参数、函数作为参数以及 defer 等知识。普通函数通常都有一个名字:

go 复制代码
func add(a, b int) int {
	return a + b
}

然后通过函数名调用:

sql 复制代码
result := add(10, 20)

但是实际开发中,有些函数可能只使用一次,或者只是临时作为另一个函数的参数。如果每次都单独定义函数名称,会产生很多只使用一次的小函数。Go 因此支持匿名函数 Anonymous Function。顾名思义,匿名函数就是没有名字的函数。匿名函数在回调、并发、延迟执行以及闭包等场景中非常常见。

一、什么是匿名函数

普通函数:

csharp 复制代码
func hello() {
	fmt.Println("Hello Go")
}

匿名函数:

go 复制代码
func() {
	fmt.Println("Hello Go")
}

可以看到,它没有:

复制代码
hello

这样的函数名称。

基本语法:

go 复制代码
func(参数列表) 返回值 {
	函数体
}

由于匿名函数没有名称,因此通常需要立即调用、赋值给变量或者作为参数传递。

二、立即执行匿名函数

最简单的使用方式:

go 复制代码
package main
import "fmt"
func main() {
	func() {
		fmt.Println("Hello Go")
	}()
}

注意最后的:

scss 复制代码
()

表示立即调用这个匿名函数。

可以理解为先定义:

go 复制代码
func() {
	fmt.Println("Hello Go")
}

然后马上执行。

这种写法通常称为立即执行函数。

三、匿名函数也可以有参数

匿名函数与普通函数一样可以接收参数:

go 复制代码
func(name string) {
	fmt.Println("Hello", name)
}("Tom")

输出:

复制代码
Hello Tom

这里:

go 复制代码
func(name string) {
	fmt.Println("Hello", name)
}

负责定义函数。

后面的:

arduino 复制代码
("Tom")

负责调用函数并传入参数。

多个参数:

css 复制代码
func(a, b int) {
	fmt.Println(a + b)
}(10, 20)

输出:

复制代码
30

四、匿名函数也可以有返回值

例如:

go 复制代码
result := func(a, b int) int {
	return a + b
}(10, 20)
fmt.Println(result)

输出:

复制代码
30

执行过程可以理解为:

css 复制代码
定义匿名函数
↓
传入 10 和 20
↓
执行 a + b
↓
返回 30
↓
赋值给 result

匿名函数和普通函数一样,也可以拥有多个返回值:

perl 复制代码
sum, sub := func(a, b int) (int, int) {
	return a + b, a - b
}(20, 10)
fmt.Println(sum, sub)

输出:

复制代码
30 10

五、把匿名函数保存到变量

匿名函数不一定需要立即执行,也可以保存到变量中:

go 复制代码
add := func(a, b int) int {
	return a + b
}

此时:

csharp 复制代码
add

就是一个函数变量。

调用:

sql 复制代码
result := add(10, 20)
fmt.Println(result)

输出:

复制代码
30

也可以多次调用:

csharp 复制代码
fmt.Println(add(1, 2))
fmt.Println(add(10, 20))
fmt.Println(add(100, 200))

这时候虽然函数本身没有名字,但变量 add 可以用来引用这个函数。

六、函数类型

例如:

go 复制代码
add := func(a, b int) int {
	return a + b
}

它的类型是:

go 复制代码
func(int, int) int

可以查看:

csharp 复制代码
fmt.Printf("%T\n", add)

输出:

go 复制代码
func(int, int) int

也可以提前声明函数变量:

go 复制代码
var operation func(int, int) int

然后赋值:

go 复制代码
operation = func(a, b int) int {
	return a + b
}

调用:

less 复制代码
fmt.Println(operation(10, 20))

这说明在 Go 中,函数也是一种值,可以保存到变量中。

七、根据条件选择不同函数

函数变量可以根据条件保存不同逻辑:

go 复制代码
var operation func(int, int) int
operator := "+"
if operator == "+" {
	operation = func(a, b int) int {
		return a + b
	}
} else {
	operation = func(a, b int) int {
		return a - b
	}
}
fmt.Println(operation(20, 10))

当:

ini 复制代码
operator == "+"

结果为:

复制代码
30

如果选择减法,就可以执行另外一个匿名函数。

这种方式可以避免大量重复的条件判断。

八、匿名函数作为参数

匿名函数非常重要的应用场景,就是作为参数传递。

先定义:

go 复制代码
func calculate(a, b int, operation func(int, int) int) int {
	return operation(a, b)
}

调用时直接传匿名函数:

go 复制代码
result := calculate(10, 20, func(a, b int) int {
	return a + b
})
fmt.Println(result)

输出:

复制代码
30

如果需要乘法:

go 复制代码
result := calculate(10, 20, func(a, b int) int {
	return a * b
})

同一个 calculate() 就可以执行不同计算逻辑。

这种作为参数传入的函数通常也叫回调函数 Callback。

九、什么是回调函数

例如:

go 复制代码
func process(value int, callback func(int)) {
	fmt.Println("开始处理")
	callback(value)
	fmt.Println("处理完成")
}

调用:

go 复制代码
process(100, func(value int) {
	fmt.Println("数据:", value)
})

输出:

复制代码
开始处理
数据: 100
处理完成

process() 不需要知道具体如何处理数据,只需要在合适的时候调用:

scss 复制代码
callback(value)

真正的处理逻辑由调用方决定。

这种设计可以提高代码灵活性。

十、匿名函数处理 Slice

例如有一个 Slice:

go 复制代码
nums := []int{1, 2, 3, 4, 5}

定义一个处理函数:

go 复制代码
func each(nums []int, callback func(int)) {
	for _, num := range nums {
		callback(num)
	}
}

调用:

go 复制代码
each(nums, func(num int) {
	fmt.Println(num * 2)
})

输出:

复制代码
2
4
6
8
10

如果希望计算平方,只需要换匿名函数:

dart 复制代码
each(nums, func(num int) {
	fmt.Println(num * num)
})

而不需要修改 each()。

十一、匿名函数实现过滤

可以进一步实现:

go 复制代码
func filter(nums []int, condition func(int) bool) []int {
	result := make([]int, 0)
	for _, num := range nums {
		if condition(num) {
			result = append(result, num)
		}
	}
	return result
}

筛选偶数:

go 复制代码
nums := []int{1, 2, 3, 4, 5, 6}
result := filter(nums, func(num int) bool {
	return num%2 == 0
})
fmt.Println(result)

输出:

csharp 复制代码
[2 4 6]

筛选大于 3 的数字:

go 复制代码
result := filter(nums, func(num int) bool {
	return num > 3
})

输出:

csharp 复制代码
[4 5 6]

可以看到,filter() 负责通用流程,而匿名函数负责具体条件。

十二、匿名函数与 sort

标准库中也经常需要传递函数。

例如对 Slice 自定义排序:

go 复制代码
package main
import (
	"fmt"
	"sort"
)
func main() {
	nums := []int{5, 2, 8, 1, 3}
	sort.Slice(nums, func(i, j int) bool {
		return nums[i] < nums[j]
	})
	fmt.Println(nums)
}

输出:

csharp 复制代码
[1 2 3 5 8]

如果希望从大到小:

go 复制代码
sort.Slice(nums, func(i, j int) bool {
	return nums[i] > nums[j]
})

这里的匿名函数告诉 sort.Slice() 两个元素应该如何比较。

十三、Struct 自定义排序

例如:

go 复制代码
type User struct {
	Name string
	Age  int
}

数据:

css 复制代码
users := []User{
	{Name: "Tom", Age: 25},
	{Name: "Jack", Age: 20},
	{Name: "Lucy", Age: 30},
}

按照年龄排序:

go 复制代码
sort.Slice(users, func(i, j int) bool {
	return users[i].Age < users[j].Age
})

这种场景非常适合匿名函数,因为排序规则通常只在当前位置使用,没有必要单独定义一个普通函数。

十四、匿名函数与 defer

defer 也可以配合匿名函数:

go 复制代码
func test() {
	defer func() {
		fmt.Println("函数结束")
	}()
	fmt.Println("执行任务")
}

输出:

复制代码
执行任务
函数结束

匿名函数内部还可以执行多条清理逻辑:

go 复制代码
defer func() {
	fmt.Println("释放资源")
	fmt.Println("记录日志")
	fmt.Println("清理临时数据")
}()

相比只能延迟一个简单调用,这种方式更加灵活。

十五、匿名函数与 goroutine

后面学习 Go 并发时,会经常看到:

go 复制代码
go func() {
	fmt.Println("执行任务")
}()

这里:

go 复制代码
func() {
	fmt.Println("执行任务")
}

是匿名函数。

前面的:

go 复制代码
go

表示启动一个 goroutine 执行它。

还可以传参数:

go 复制代码
go func(id int) {
	fmt.Println("任务:", id)
}(1)

匿名函数与 goroutine 的组合是 Go 并发开发中非常常见的写法。

十六、匿名函数访问外部变量

匿名函数可以直接访问它所在作用域中的变量。

例如:

go 复制代码
func main() {
	name := "Tom"
	func() {
		fmt.Println(name)
	}()
}

输出:

复制代码
Tom

匿名函数中没有定义 name,却可以访问外部:

go 复制代码
name := "Tom"

甚至可以修改:

go 复制代码
count := 0
func() {
	count++
}()
fmt.Println(count)

输出:

复制代码
1

这已经涉及匿名函数一个非常重要的能力------闭包 Closure。

十七、匿名函数的变量生命周期

看下面的代码:

go 复制代码
func test() func() {
	name := "Tom"
	return func() {
		fmt.Println(name)
	}
}

调用:

css 复制代码
f := test()
f()

虽然 test() 已经执行结束,但是返回的匿名函数仍然可以访问:

复制代码
name

这是因为这个变量被返回的函数引用了。

这种"函数和它引用的外部变量组合在一起"的机制,就是闭包。

下一篇会专门详细讲解。

十八、立即执行函数的实际用途

有时候我们只是希望创建一个临时作用域:

scss 复制代码
func() {
	config := "debug"
	fmt.Println(config)
}()

config 只存在于匿名函数内部,不会污染外部作用域。

也可以执行初始化逻辑:

go 复制代码
result := func() int {
	a := 10
	b := 20
	return a + b
}()

相比把大量临时变量放到外部,匿名函数可以让变量作用范围更加明确。

十九、匿名函数不能直接递归的问题

下面这种写法:

go 复制代码
factorial := func(n int) int {
	return n * factorial(n-1)
}

会遇到定义阶段引用变量的问题。

如果需要匿名函数递归,可以先声明函数变量:

go 复制代码
var factorial func(int) int
factorial = func(n int) int {
	if n <= 1 {
		return 1
	}
	return n * factorial(n-1)
}

调用:

less 复制代码
fmt.Println(factorial(5))

输出:

复制代码
120

不过如果一个递归函数本身具有明确业务意义,通常定义成普通命名函数会更加清晰。

二十、普通函数和匿名函数怎么选择

如果函数会在多个地方使用:

csharp 复制代码
func calculatePrice() {}

通常定义普通函数。

如果逻辑只在当前位置使用:

go 复制代码
sort.Slice(users, func(i, j int) bool {
	return users[i].Age < users[j].Age
})

使用匿名函数更加方便。

可以简单理解:

go 复制代码
普通函数:
适合重复使用
逻辑相对独立
具有明确业务含义
匿名函数:
适合临时逻辑
适合回调
适合函数参数
适合 defer
适合 goroutine

不要为了减少函数名称而把几十行甚至上百行代码全部塞进匿名函数,否则反而会降低可读性。

二十一、匿名函数实战:通用计算器

例如:

go 复制代码
package main
import "fmt"
func calculate(a, b int, fn func(int, int) int) int {
	return fn(a, b)
}
func main() {
	add := calculate(20, 10, func(a, b int) int {
		return a + b
	})
	sub := calculate(20, 10, func(a, b int) int {
		return a - b
	})
	mul := calculate(20, 10, func(a, b int) int {
		return a * b
	})
	fmt.Println(add)
	fmt.Println(sub)
	fmt.Println(mul)
}

输出:

复制代码
30
10
200

calculate() 不关心具体计算方式,真正的算法由匿名函数决定。这就是函数作为值带来的灵活性。

二十二、常见错误

第一个错误是立即执行匿名函数忘记最后的 ():

go 复制代码
func() {
	fmt.Println("Hello")
}

如果希望立即执行,应该:

scss 复制代码
func() {
	fmt.Println("Hello")
}()

第二个错误是函数签名不匹配:

go 复制代码
var f func(int) int

就不能赋值:

go 复制代码
f = func(name string) string {
	return name
}

第三个错误是匿名函数过于复杂。如果逻辑很长、需要多处调用或者具有明确业务意义,应该提取为普通函数。

第四个错误是忽略匿名函数可以访问外部变量。尤其以后结合 goroutine 使用时,需要特别关注变量捕获和并发安全问题。

第五个错误是把匿名函数和普通代码块混淆。匿名函数本质上仍然是函数,拥有自己的参数、返回值和局部变量。

二十三、总结

定义匿名函数:

go 复制代码
func() {
	fmt.Println("Hello")
}

立即调用:

scss 复制代码
func() {
	fmt.Println("Hello")
}()

带参数:

go 复制代码
func(name string) {
	fmt.Println(name)
}("Tom")

带返回值:

go 复制代码
result := func(a, b int) int {
	return a + b
}(10, 20)

保存到变量:

go 复制代码
add := func(a, b int) int {
	return a + b
}

作为参数:

go 复制代码
calculate(10, 20, func(a, b int) int {
	return a + b
})

配合 defer:

go 复制代码
defer func() {
	fmt.Println("结束")
}()

配合 goroutine:

go 复制代码
go func() {
	fmt.Println("任务执行")
}()

学习匿名函数重点掌握:

go 复制代码
1.匿名函数没有函数名称
2.匿名函数可以立即执行
3.匿名函数可以保存到变量
4.匿名函数可以拥有参数和返回值
5.函数可以作为另一个函数的参数
6.匿名函数非常适合实现回调
7.匿名函数常用于 defer 和 goroutine
8.匿名函数可以访问外部作用域变量

最后一点尤其重要。下面这段代码:

scss 复制代码
count := 0
add := func() {
	count++
}
add()
add()
fmt.Println(count)

为什么匿名函数每次执行都能够找到并修改外部的 count?如果一个函数已经执行结束,它内部定义的变量为什么有时候仍然不会消失?这些问题背后就是 Go 中另一个非常重要的函数概念------闭包 Closure 。

下一篇:闭包 Closure------函数如何记住外部变量

相关推荐
怕浪猫3 小时前
外包和大厂的前端代码,差在凌晨三点
前端·面试·github
金銀銅鐵3 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路4 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x5 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn6 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈7 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780518 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123458 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒8 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记8 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端