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

14 阅读10分钟

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

func add(a, b int) int {
	return a + b
}

然后通过函数名调用:

result := add(10, 20)

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

一、什么是匿名函数

普通函数:

func hello() {
	fmt.Println("Hello Go")
}

匿名函数:

func() {
	fmt.Println("Hello Go")
}

可以看到,它没有:

hello

这样的函数名称。
基本语法:

func(参数列表) 返回值 {
	函数体
}

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

二、立即执行匿名函数

最简单的使用方式:

package main
import "fmt"
func main() {
	func() {
		fmt.Println("Hello Go")
	}()
}

注意最后的:

()

表示立即调用这个匿名函数。
可以理解为先定义:

func() {
	fmt.Println("Hello Go")
}

然后马上执行。
这种写法通常称为立即执行函数

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

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

func(name string) {
	fmt.Println("Hello", name)
}("Tom")

输出:

Hello Tom

这里:

func(name string) {
	fmt.Println("Hello", name)
}

负责定义函数。
后面的:

("Tom")

负责调用函数并传入参数。
多个参数:

func(a, b int) {
	fmt.Println(a + b)
}(10, 20)

输出:

30

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

例如:

result := func(a, b int) int {
	return a + b
}(10, 20)
fmt.Println(result)

输出:

30

执行过程可以理解为:

定义匿名函数
↓
传入 1020
↓
执行 a + b
↓
返回 30
↓
赋值给 result

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

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

输出:

30 10

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

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

add := func(a, b int) int {
	return a + b
}

此时:

add

就是一个函数变量。
调用:

result := add(10, 20)
fmt.Println(result)

输出:

30

也可以多次调用:

fmt.Println(add(1, 2))
fmt.Println(add(10, 20))
fmt.Println(add(100, 200))

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

六、函数类型

例如:

add := func(a, b int) int {
	return a + b
}

它的类型是:

func(int, int) int

可以查看:

fmt.Printf("%T\n", add)

输出:

func(int, int) int

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

var operation func(int, int) int

然后赋值:

operation = func(a, b int) int {
	return a + b
}

调用:

fmt.Println(operation(10, 20))

这说明在 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))

当:

operator == "+"

结果为:

30

如果选择减法,就可以执行另外一个匿名函数。
这种方式可以避免大量重复的条件判断。

八、匿名函数作为参数

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

func calculate(a, b int, operation func(int, int) int) int {
	return operation(a, b)
}

调用时直接传匿名函数:

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

输出:

30

如果需要乘法:

result := calculate(10, 20, func(a, b int) int {
	return a * b
})

同一个 calculate() 就可以执行不同计算逻辑。
这种作为参数传入的函数通常也叫回调函数 Callback

九、什么是回调函数

例如:

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

调用:

process(100, func(value int) {
	fmt.Println("数据:", value)
})

输出:

开始处理
数据: 100
处理完成

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

callback(value)

真正的处理逻辑由调用方决定。
这种设计可以提高代码灵活性。

十、匿名函数处理 Slice

例如有一个 Slice:

nums := []int{1, 2, 3, 4, 5}

定义一个处理函数:

func each(nums []int, callback func(int)) {
	for _, num := range nums {
		callback(num)
	}
}

调用:

each(nums, func(num int) {
	fmt.Println(num * 2)
})

输出:

2
4
6
8
10

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

each(nums, func(num int) {
	fmt.Println(num * num)
})

而不需要修改 each()

十一、匿名函数实现过滤

可以进一步实现:

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
}

筛选偶数:

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

输出:

[2 4 6]

筛选大于 3 的数字:

result := filter(nums, func(num int) bool {
	return num > 3
})

输出:

[4 5 6]

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

十二、匿名函数与 sort

标准库中也经常需要传递函数。
例如对 Slice 自定义排序:

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)
}

输出:

[1 2 3 5 8]

如果希望从大到小:

sort.Slice(nums, func(i, j int) bool {
	return nums[i] > nums[j]
})

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

十三、Struct 自定义排序

例如:

type User struct {
	Name string
	Age  int
}

数据:

users := []User{
	{Name: "Tom", Age: 25},
	{Name: "Jack", Age: 20},
	{Name: "Lucy", Age: 30},
}

按照年龄排序:

sort.Slice(users, func(i, j int) bool {
	return users[i].Age < users[j].Age
})

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

十四、匿名函数与 defer

defer 也可以配合匿名函数:

func test() {
	defer func() {
		fmt.Println("函数结束")
	}()
	fmt.Println("执行任务")
}

输出:

执行任务
函数结束

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

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

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

十五、匿名函数与 goroutine

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

go func() {
	fmt.Println("执行任务")
}()

这里:

func() {
	fmt.Println("执行任务")
}

是匿名函数。
前面的:

go

表示启动一个 goroutine 执行它。
还可以传参数:

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

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

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

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

func main() {
	name := "Tom"
	func() {
		fmt.Println(name)
	}()
}

输出:

Tom

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

name := "Tom"

甚至可以修改:

count := 0
func() {
	count++
}()
fmt.Println(count)

输出:

1

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

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

看下面的代码:

func test() func() {
	name := "Tom"
	return func() {
		fmt.Println(name)
	}
}

调用:

f := test()
f()

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

name

这是因为这个变量被返回的函数引用了。
这种“函数和它引用的外部变量组合在一起”的机制,就是闭包。
下一篇会专门详细讲解。

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

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

func() {
	config := "debug"
	fmt.Println(config)
}()

config 只存在于匿名函数内部,不会污染外部作用域。
也可以执行初始化逻辑:

result := func() int {
	a := 10
	b := 20
	return a + b
}()

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

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

下面这种写法:

factorial := func(n int) int {
	return n * factorial(n-1)
}

会遇到定义阶段引用变量的问题。
如果需要匿名函数递归,可以先声明函数变量:

var factorial func(int) int
factorial = func(n int) int {
	if n <= 1 {
		return 1
	}
	return n * factorial(n-1)
}

调用:

fmt.Println(factorial(5))

输出:

120

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

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

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

func calculatePrice() {}

通常定义普通函数。
如果逻辑只在当前位置使用:

sort.Slice(users, func(i, j int) bool {
	return users[i].Age < users[j].Age
})

使用匿名函数更加方便。
可以简单理解:

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

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

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

例如:

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() 不关心具体计算方式,真正的算法由匿名函数决定。这就是函数作为值带来的灵活性。

二十二、常见错误

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

func() {
	fmt.Println("Hello")
}

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

func() {
	fmt.Println("Hello")
}()

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

var f func(int) int

就不能赋值:

f = func(name string) string {
	return name
}

第三个错误是匿名函数过于复杂。如果逻辑很长、需要多处调用或者具有明确业务意义,应该提取为普通函数。
第四个错误是忽略匿名函数可以访问外部变量。尤其以后结合 goroutine 使用时,需要特别关注变量捕获和并发安全问题。
第五个错误是把匿名函数和普通代码块混淆。匿名函数本质上仍然是函数,拥有自己的参数、返回值和局部变量。

二十三、总结

定义匿名函数:

func() {
	fmt.Println("Hello")
}

立即调用:

func() {
	fmt.Println("Hello")
}()

带参数:

func(name string) {
	fmt.Println(name)
}("Tom")

带返回值:

result := func(a, b int) int {
	return a + b
}(10, 20)

保存到变量:

add := func(a, b int) int {
	return a + b
}

作为参数:

calculate(10, 20, func(a, b int) int {
	return a + b
})

配合 defer

defer func() {
	fmt.Println("结束")
}()

配合 goroutine:

go func() {
	fmt.Println("任务执行")
}()

学习匿名函数重点掌握:

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

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

count := 0
add := func() {
	count++
}
add()
add()
fmt.Println(count)

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