Golang遵循”少即是多”的设计哲学,同时又支持闭包(Closure),那么闭包对Golang来说肯定有重要的价值。
闭包是由函数及其相关的引用环境组合而成的实体,即闭包 = 函数 + 引用环境。

函数

函数是一段可执行代码,编译后就”固化”了,每个函数在内存中只有一份实例,得到函数的入口点便可以执行函数了。在函数式编程语言中,函数是一等公民,函数可以作为另一个函数的参数或返回值,可以赋给一个变量。函数可以嵌套定义,即在一个函数内部可以定义另一个函数,有了嵌套函数这种结构,便会产生闭包。

在面向对象编程中,我们将对象传来传去,而在函数式编程中,我们将函数传来传去。在函数式编程中,高阶函数是至少满足以下两点的函数:

  • 函数可以作为参数被传递

  • 函数可以作为返回值输出

匿名函数

匿名函数是指不需要定义函数名的一种函数实现方式,它并不是一个新概念,最早可以回溯到1958年的Lisp语言。但是由于各种原因,C和C++一直都没有对匿名函数给予支持。
匿名函数由一个不带函数名的函数声明和函数体组成,比如:

  1. func(xy int) int {
  2. return x + y
  3. }

在Golang中,所有函数都是值类型,既可以作为参数传递,又可以作为返回值传递。
匿名函数可以赋值给一个变量:

  1. f := func() int {
  2. ...
  3. }

我们可以定义一种函数类型:

  1. type CalcFunc func(x, y int) int

函数可以作为值传递:

  1. func AddFunc(x, y int) int {
  2. return x + y
  3. }
  4. func SubFunc(x, y int) int {
  5. return x - y
  6. }
  7. ...
  8. func OperationFunc(x, y int, calcFunc CalcFunc) int {
  9. return calcFunc(x, y)
  10. }
  11. func main() {
  12. sum := OperationFunc(1, 2, AddFunc)
  13. difference := OperationFunc(1, 2, SubFunc)
  14. ...
  15. }

函数可以作为返回值:

  1. // 第一种写法
  2. func add(x, y int) func() int {
  3. f := func() int {
  4. return x + y
  5. }
  6. return f
  7. }
  8. // 第二种写法
  9. func add(x, y int) func() int {
  10. return func() int {
  11. return x + y
  12. }
  13. }

当函数返回多个匿名函数时,建议采用第一种写法:

  1. func calc(x, y int) func(int), func()) {
  2. f1 := func(z int) int {
  3. return (x + y) * z / 2
  4. }
  5. f2 := func() int {
  6. return 2 * (x + y)
  7. }
  8. return f1, f2
  9. }

匿名函数的调用有两种方法:

  1. / 通过返回值调用
  2. func main() {
  3. f1, f2 := calc(2, 3)
  4. n1 := f1(10)
  5. n2 := f1(20)
  6. n3 := f2()
  7. fmt.Println("n1, n2, n3:", n1, n2, n3)
  8. }
  9. // 在匿名函数定义的同时进行调用:花括号后跟参数列表表示函数调用
  10. func safeHandler() {
  11. defer func() {
  12. err := recover()
  13. if err != nil {
  14. fmt.Println("some exception has happend:", err)
  15. }
  16. }()
  17. ...
  18. }

闭包的本质

闭包是包含变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义。由于自由变量包含在代码块中,所以只要闭包还被使用,那么这些自由变量以及它们引用的对象就不会被释放,要执行的代码为自由变量提供绑定的计算环境。

闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递到其他函数,最重要的是能够被函数动态创建和返回。

Golang中的闭包同样也会引用到函数外的变量,闭包的实现确保只要闭包被使用,那么被闭包引用的变量会一直存在。从形式上看,匿名函数都是闭包。

示例如下:

  1. func add(n int) func(int) int {
  2. sum := n
  3. f := func(x int) int {
  4. var i int = 2
  5. sum += i * x
  6. return sum
  7. }
  8. return f
  9. }
  10. func main() {
  11. f1 := add(10)
  12. n11 := f1(3)
  13. n12 := f1(6)
  14. f2 := add(20)
  15. n21 := f2(4)
  16. n22 := f2(8)
  17. }

该例子中函数变量为f,自由变量为sum,同时f为sum提供绑定的计算环境,使得sum和f粘滞在了一起,它们组成的代码块就是闭包。add函数的返回值是一个闭包,而不仅仅是f函数的地址。在该闭包函数中,只有内部的匿名函数f才能访问局部变量i,而无法通过其他路径访问,因此闭包保证了i的安全性。
当我们分别用不同参数(10,20)注入add函数而得到不同的闭包函数变量时,得到的结果是隔离的,也就是说每次调用add函数后都将生产并保存一个新的局部变量sum。

按照命令式语言的规则,add函数只是返回了内嵌函数f的地址,但是执行f函数时将会由于在其作用域内找不到sum变量而出错。而在函数式语言中,当内嵌函数体内引用到体外的变量时,就会把定义时涉及到的引用环境和函数体打包成一个整体返回。闭包的使用和正常的函数调用没有区别。

在程序执行中的某个点所有处于活跃状态的约束所组成的集合,其中的约束指的是一个变量的名字和其所代表的对象之间的联系。

当每次调用add函数时都将返回一个新的闭包实例,这些实例是隔离的,分别包含调用时不同的引用环境现场。不同于函数,闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合产生不同的实例。

其实我们可以将闭包函数看成一个类(C++),一个闭包函数调用就是实例化一个类,闭包的自由变量就是类的成员变量,闭包函数的参数就是类的函数对象的参数。在该例子中,f1和f2可以看作是实例化的两个对象,ni1和ni2(i=1,2)分别可以看作是函数对象的两次调用(参数不同)的返回值。

对象是附有行为的数据,而闭包是附有数据的行为。

闭包的应用

避免程序运行时异常崩溃
Golang中对于一般的错误处理提供了error接口,对于不可预见的错误(异常)处理提供了两个内置函数panic和recover。error接口类似C/C++的错误码,panic和recover类似于C++的try/catch/throw。

当一个函数执行过程中调用panic函数时,正常的函数执行流程将立即终止,但函数中之前使用defer关键字延迟执行的语句将正常展开执行,之后该函数将返回到调用函数,并导致逐层向上执行panic流程,直至所属的goroutine中所有正在执行的函数被终止。错误信息将被报告,包括在调用panic()函数时传入的参数,这个过程称为异常处理流程。

recover函数用于终止错误处理流程。一般情况下,recover应该在一个使用defer关键字的函数中执行以有效截取错误处理流程,如果没有在发生异常的goroutine中明确调用恢复过程(调用recover函数),会导致该goroutine所属的进程打印异常信息后直接退出。

对于第三方库的调用,在不清楚是否有panic的情况下,最后在适配层统一加上recover过程,否则会导致当前进程的异常退出,而这并不是我们所期待的。

示例如下:

  1. func thirdPartyAdaptedHandler(...) {
  2. defer func() {
  3. err := recover()
  4. if err != nil {
  5. fmt.Println("some exception has happend:", err)
  6. }
  7. }()
  8. ...
  9. }

我们定义了一个名为safeHandler的函数,将所有的业务逻辑处理函数(listHandler、viewHandler和uploadHandler)进行一次包装。safeHandler函数有一个参数并且返回一个值,传入的参数和返回值都是一个函数,且都是http.HandlerFunc类型,这种类型的函数有两个参数:http.ResponseWriter和 *http.Request。事实上,我们正是要把业务逻辑处理函数作为参数传入到safeHandler()方法中,这样任何一个错误处理流程向上回溯的时候,我们都能对其进行拦截处理,从而也能避免程序停止运行。

  1. func safeHandler(fn http.HandlerFunc) http.HandlerFunc {
  2. return func(w http.ResponseWriter, r *http.Request) {
  3. defer func() {
  4. if e, ok := recover().(error); ok {
  5. http.Error(w, err.Error(), http.StatusInternalServerError)
  6. fmt.Println("WARN: panic in %v - %v", fn, e)
  7. fmt.Println(string(debug.Stack()))
  8. }
  9. }()
  10. fn(w, r)
  11. }
  12. }

模版方法

定义一个转换函数类型:

  1. type Traveser func(ele interface{})

Process函数功能:对切片array进行了traveser处理

  1. func Process(array interface{}, traveser Traveser) error {
  2. ...
  3. traveser(array)
  4. ...
  5. return nil
  6. }

SortByAscending函数功能:升序排序函数切片中的数据:

  1. func SortByAscending(ele interface{}) {
  2. ...
  3. }

SortByDescending函数功能:降序排序数据切片中的数据:

  1. func SortByDescending(ele interface{}) {
  2. ...
  3. }

Process函数调用:

  1. func main() {
  2. intSlice := make([]int, 0)
  3. intSlice = append(intSlice, 3, 1, 4, 2)
  4. Process(intSlice, SortByDescending)
  5. fmt.Println(intSlice) //[4 3 2 1]
  6. Process(intSlice, SortByAscending)
  7. fmt.Println(intSlice) //[1 2 3 4]
  8. }

模板方法模式是定义一个操作中的算法的框架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的框架就可重新定义该算法的某些特定步骤。在Golang中,模板方法不但可以通过interface和组合的方式实现,而且可以通过闭包的方式实现。


Go 闭包 - 图1