Go中的匿名函数与闭包

简介: Go中的匿名函数与闭包

关键词:


函数式编程  闭包  匿名函数  匿名函数特别适合作为函数或方法的回调


在Go中函数是一等公民,和string,int等一样。 而在C、C++ 等不支持匿名函数的语言中,函数不能在运行期创建

go 学习笔记之仅仅需要一个示例就能讲清楚什么闭包




闭包 与 普通函数的区别


在(普通)函数里面定义一个内部函数(匿名函数),并且这个内部函数(匿名函数)用到了外面(普通)函数的变量,那么将这个内部函数和用到的一些变量统称为闭包

  • 在闭包中,既有函数,又有数据,而且(其内部定义的)数据是闭包里面独有的数据,与外界无影响;
  • (普通)函数中,需要使用的全局变量,在一定程度上是受到限制的,因为全局变量不仅仅是一个函数使用,其他的函数也可能会使用到,一旦修改会影响到其他函数使用全局变量,所以全局变量不能随便修改从而在函数的使用中受到一定局限性




匿名函数和闭包的关系


简单来说匿名函数是指不需要定义函数名的一种函数实现方式。匿名函数是由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必声明(一个子方法)所以(在某些场景下)被广泛使用

关于闭包的定义存在以下广泛流传的公式:闭包=函数+引用环境。函数指的是匿名函数,引用环境指的是编译器发现闭包,直接将闭包引用的外部变量在堆上分配空间;当闭包引用了函数的内部变量(即局部变量)时,每次调用的外部变量数据都会跟随闭包的变化而变化,闭包函数和外部变量是共享的。

显然,闭包只能通过匿名函数实现,可以把闭包看作是有状态的匿名函数,反过来,如果匿名函数引用了外部变量,就形成了一个闭包

Go 函数式编程篇(三):匿名函数和闭包

一般来说,一个函数返回另外一个函数,这个被返回的函数可以引用外层函数的局部变量,这形成了一个闭包。在Go中,「闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的上下文环境(相当于一个符号查找表)」

type closure struct {
   F uintptr   // 函数指针,代表着内部匿名函数
   x *int      // 自由变量x,代表着对外部环境的引用
}

Go函数闭包底层实现

在Go,PHP中,匿名函数可以认为就是闭包(Go 规范和 FAQ 都这么说了 ),哪怕这个匿名函数没有入参,没有引用外部的变量,也没有任何返回值,如

go

复制代码

func(){
    print(123)
    }()

严格来说,这其实只是个匿名函数, 不算闭包。

但Go里称其为闭包也ok,即模糊了匿名函数和闭包的界限(有引用外部变量的匿名函数为闭包)


一道 Go 闭包题,面试官说原来自己答错了:面别人也涨知识




一些例子


无参数也无返回值的匿名函数

package main
import (
  "fmt"
)
func main() {
  f := func() {
    fmt.Println("不加括号就只是定义,赋值给f,可通过f()来调用")
  }
  f()
  fmt.Printf("变量f的类型为: %T\n", f) // func()
  // 下面这种方式定义,只在此调用一次,不如上面的方式,可以随时复用
  fmt.Println("--------------")
  func() {
    fmt.Println("而加上最后加上()就是直接调用(这种方式只能在此调用一次,没法复用了)")
  }()
}

输出:

不加括号就只是定义,赋值给f,可通过f()来调用
变量f的类型为: func()
--------------
而加上最后加上()就是直接调用(这种方式只能在此调用一次,没法复用了)

带参数的匿名函数

package main
import (
  "fmt"
)
func main() {
  i := 0
  // 后面有(),一次执行
  func(i int) {
    fmt.Println(i + 1)
  }(i)
  i = -100000
  // 赋值给add,可通过add()方式多次调用
  add := func(k int) {
    fmt.Println(k + 6)
  }
  add(200)
}

输出:

1
206

配合defer,可以使问题非常复杂。也是高阶面试常问的~

变形1:

package main
import (
  "fmt"
)
func main() {
  i := 0
  // 后面有(),一次执行
  defer func(i int) {
    fmt.Println(i + 1)
  }(i)
  i = -100000
  // 赋值给add,可通过add()方式多次调用
  add := func(k int) {
    fmt.Println(k + 6)
  }
  add(200)
}

输出:

206
1

目前还好理解,defer在return时执行(确切地说,是在return和计算return值的中间执行)


变形2:

package main
import (
  "fmt"
)
func main() {
  i := 0
  // 后面有(),一次执行
  defer func(k int) {
    fmt.Println(i + 1)
  }(i)
  i = -100000
  // 赋值给add,可通过add()方式多次调用
  add := func(k int) {
    fmt.Println(k + 6)
  }
  add(200)
}

输出:

206
-99999

如果有人说Go简单,可以请其解释一下这个输出..


有返回值的匿名函数

package main
import "fmt"
func main() {
  name := "张三"
  say := func(name string) string {
    return "hello " + name
  }
  res := say(name)
  fmt.Println(res) //hello 张三
}

当返回值是匿名函数

package main
import "fmt"
func main() {
  a := Fun()
  b := a("hello ")
  c := a("hello ")
  d := Fun()
  e := d("hello ")
  f := d("hello ")
  fmt.Println(b) //world+hello
  fmt.Println(c) //world+hello hello
  fmt.Println(e) //world+hello
  fmt.Println(f) //world+hello hello
}
func Fun() func(string) string {
  rs := "world+"
  return func(args string) string {
    rs += args
    return rs
  }
}

等同于

package main
import "fmt"
func main() {
  cui := func() func(string) string {
    rs := "world+"
    return func(args string) string {
      rs += args
      return rs
    }
  }
  a := cui()
  b := a("hello ")
  c := a("hello ")
  d := cui()
  e := d("hello ")
  f := d("hello ")
  fmt.Println(b) //world+hello
  fmt.Println(c) //world+hello hello
  fmt.Println(e) //world+hello
  fmt.Println(f) //world+hello hello
}

参考自 GO 匿名函数和闭包


当参数是匿名函数


参考下方回调函数:闭包可以用作回调函数(例如在异步编程中,可以捕获外部函数的上下文) && 高阶函数:闭包可以用作高阶函数的参数,并在调用时返回新的函数?(将匿名函数作为函数参数;可以让该函数执行多种不同逻辑)

多个匿名函数

package main
import "fmt"
func main() {
  f1, f2 := F(1, 2)
  fmt.Println(f1(4)) //6
  fmt.Println(f2())  //6
}
func F(x, y int) (func(int) int, func() int) {
  f1 := func(z int) int {
    return (x + y) * z / 2
  }
  f2 := func() int {
    return 2 * (x + y)
  }
  return f1, f2
}

常见使用场景



私有数据:闭包可以捕获函数内部的数据,并且对外部不可见。这是一种创建私有数据的方法(保证局部变量的安全性)

package main
import "fmt"
func main() {
  var j int = 1
  f := func() {
    var i int = 1 // i 在闭包内部定义,其值被隔离,不能从外部修改
    fmt.Printf("i, j: %d, %d\n", i, j)
  }
  f()
  j += 2
  f() // 对比下面的输出,可见并不是调用时刻的值,而只是记录变量的引用
  defer f()
  j += 10000
}

输出:

i, j: 1, 1
i, j: 1, 3
i, j: 1, 10003
package main
import (
  "fmt"
)
func main() {
  accumulator := SomeFunc() //使用accumulator变量接收一个闭包
  // 累加计数并打印
  fmt.Println("The first call CallNum is ", accumulator()) //运行结果为:The first call CallNum is 1
  // 累加计数并打印
  fmt.Println("The second call CallNum is ", accumulator()) //运行结果为:The second call CallNum is 2
}
func SomeFunc() func() int { // 创建一个函数,返回一个闭包,闭包每次调用函数会对函数内部变量进行累加
  var CallNum = 0 //函数调用次数,系函数内部变量,外部无法访问,仅当函数被调用时进行累加
  return func() int { // 返回一个闭包
    CallNum++ //对value进行累加
    //实现函数具体逻辑
    return CallNum // 返回内部变量value的值
  }
}

输出:

The first call CallNum is  1
The second call CallNum is  2

通过闭包既没有暴露CallNum这个变量,又实现了为函数计数的目的


回调函数:闭包可以用作回调函数(例如在异步编程中,可以捕获外部函数的上下文) && 高阶函数:闭包可以用作高阶函数的参数,并在调用时返回新的函数?(将匿名函数作为函数参数;可以让该函数执行多种不同逻辑)


Go基础系列:函数(2)——回调函数和闭包

参考自 【Go基础】搞懂函数回调和闭包

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。

日常开发中,可以将函数B作为另一个函数A的参数,可以使得函数A的通用性更强(可随意定义函数B,只要满足规则,函数A都可以去处理),这比较适合于回调函数。

下面看几个简单的例子来理解回调:

package main
import "fmt"
type Callback func(x, y int) int
// 提供一个接口,让外部去实现
func test1(x, y int, callback Callback) int {
  return callback(x, y)
}
// 回调函数的具体实现
func calculationXOR(x, y int) int {
  return x ^ y
}
func calculationAND(x, y int) int {
  return x & y
}
// 回调函数的具体实现
func main() {
  fmt.Println(test1(2, 3, calculationXOR)) //这样调用test1就能实现异或 以及 与的运算
  fmt.Println(test1(2, 3, calculationAND))
}
1
2

再看个简单例子:将字符串转为Int,转换失败时执行回调函数,输出错误信息

package main
import (
  "fmt"
  "strconv"
)
type Callback func(msg string)
// 将字符串转换为int64,如果转换失败调用Callback
func stringToInt(s string, callback Callback) int64 {
  if value, err := strconv.ParseInt(s, 0, 0); err != nil {
    callback(err.Error())
    return 0
  } else {
    return value
  }
}
// 记录日志消息的具体实现
func errLog(msg string) {
  fmt.Println("Convert error(转换发生了错误!): ", msg)
}
func main() {
  fmt.Println(stringToInt("18", errLog))
  fmt.Println(stringToInt("hh", errLog))
}

输出:

18
Convert error(转换发生了错误!):  strconv.ParseInt: parsing "hh": invalid syntax

下面这个例子和第一个类似:

package main
import "fmt"
func main() {
  // 普通的加法操作
  add1 := func(a, b int) int {
    return a + b
  }
  // 定义另一种加法规则(即  加数*10+第二个加数)
  base := 10
  add2 := func(a, b int) int {
    return a*base + b
  }
  handleAdd(1, 2, add1)
  handleAdd(1, 2, add2)
}
// 将匿名函数作为参数
func handleAdd(a, b int, call func(int, int) int) {
  fmt.Println(call(a, b))
}

输出:

3
12

这样就可以通过一个函数执行多种不同加法实现算法,提升代码的复用性


可以基于这个功能特性实现一些更复杂的业务逻辑,如 Go 官方 net/http 包底层的路由处理器也是这么实现的:

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  DefaultServeMux.HandleFunc(pattern, handler)
}

Go源码中还有非常多的将func作为参数的高阶函数,参数的func即回调函数,更多可参考

可通过关键字func(检索


延迟计算:闭包可以延迟计算,直到闭包被调用时才执行计算(将匿名函数作为函数返回值)

package main
import "fmt"
// 将函数作为返回值
func deferAdd(a, b int) func() int {
  return func() int {
    return a + b
  }
}
func main() {
  // 此时返回的是匿名函数
  addFunc := deferAdd(1, 2)
  // 这里才会真正执行加法操作
  fmt.Println(addFunc()) // 3
}

Go函数闭包底层实现


易错问题


循环里打印出的都是最后一个值


case1

package main
import "fmt"
func main() {
  // 此时a是F()的返回值,即一个[]func()
  //a := F()
  a := func() []func() {
    b := make([]func(), 3, 3)
    for i := 0; i < 3; i++ {
      b[i] = func() {
        fmt.Println(&i, i)
      }
    }
    return b
  }()
  a[0]() //0x140000200c8 3
  a[1]() //0x140000200c8 3
  a[2]() //0x140000200c8 3
}
//func F() []func() {
//  b := make([]func(), 3, 3)
//  for i := 0; i < 3; i++ {
//    b[i] = func() {
//      fmt.Println(&i, i)
//    }
//  }
//  return b
//}

解决办法:

每次复制变量 i 然后传到匿名函数中,让闭包的环境变量不相同。

package main
import "fmt"
func main() {
  a := F()
  a[0]() //0x14000128008 0
  a[1]() //0x14000128010 1
  a[2]() //0x14000128018 2
}
func F() []func() {
  b := make([]func(), 3, 3)
  for i := 0; i < 3; i++ {
    b[i] = (func(j int) func() {
      return func() {
        fmt.Println(&j, j)
      }
    })(i)
  }
  return b
}
//或者
//package main
//
//import "fmt"
//
//func main() {
//  a := F()
//  a[0]() //0xc00004c080 0
//  a[1]() //0xc00004c088 1
//  a[2]() //0xc00004c090 2
//}
//func F() []func() {
//  b := make([]func(), 3, 3)
//  for i := 0; i < 3; i++ {
//    j := i
//    b[i] = func() {
//      fmt.Println(&j, j)
//    }
//  }
//  return b
//}

参考自 GO 匿名函数和闭包

package main
import "fmt"
func main() {
  // 保存函数闭包
  var s []func()
  for _, v := range []string{"a", "b", "c", "d", "e"} {
    s = append(s, func() {
      // 捕获v, 保存在闭包中
      fmt.Printf("value: %v\n", v)
    })
  }
  for _, f := range s {
    f()
  }
}

输出:

value: e
value: e
value: e
value: e
value: e

闭包中捕获的v不是"值", 而是"有地址的变量"(如GoLang闭包,注意!这里有蹊跷 中图1所示),且创建闭包时,循环变量的值已经被确定,并与闭包关联。当闭包被调用时,它使用捕获的值,而不是当前值,解决的关键就在于重新声明变量,这样每个闭包都有自己的变量,能够正确地访问其所需的值


case2(for range+Goroutine 使用闭包不当)

package main
import (
  "fmt"
  "time"
)
func main() {
  tests1ice := []int{1, 2, 3, 4, 5}
  for _, v := range tests1ice {
    go func() {
      fmt.Println(v)
    }()
  }
  time.Sleep(2 * time.Second)
}
5
5
5
5
5

由于没有在Goroutine中对切片执行写操作,所以首先排除了内存屏障的问题,最终还是通过反编译查看汇编代码,发现Goroutine打印的变量v,其实是地址引用,Goroutine执行的时候变量v所在地址所对应的值已经发生了变化,汇编代码如下:

for _, v := range tests1ice {
  499224:       48 8d 05 f5 af 00 00    lea    0xaff5(%rip),%rax        # 4a4220 <type.*+0xa220>
  49922b:       48 89 04 24             mov    %rax,(%rsp)
  49922f:       e8 8c 3a f7 ff          callq  40ccc0 <runtime.newobject>
  499234:       48 8b 44 24 08          mov    0x8(%rsp),%rax
  499239:       48 89 44 24 48          mov    %rax,0x48(%rsp)
  49923e:       31 c9                   xor    %ecx,%ecx
  499240:       eb 3e                   jmp    499280 <main.main+0xc0>
  499242:       48 89 4c 24 18          mov    %rcx,0x18(%rsp)
  499247:       48 8b 54 cc 20          mov    0x20(%rsp,%rcx,8),%rdx
  49924c:       48 89 10                mov    %rdx,(%rax)
                go func() {
  49924f:       c7 04 24 08 00 00 00    movl   $0x8,(%rsp)
  499256:       48 8d 15 f3 b7 02 00    lea    0x2b7f3(%rip),%rdx        # 4c4a50 <go.func.*+0x6c>
  49925d:       48 89 54 24 08          mov    %rdx,0x8(%rsp)
  499262:       48 89 44 24 10          mov    %rax,0x10(%rsp)
  499267:       e8 54 3a fa ff          callq  43ccc0 <runtime.newproc>

解决方案一:在参数方式向匿名函数传递值引用

package main
import (
  "fmt"
  "time"
)
func main() {
  tests1ice := []int{1, 2, 3, 4, 5}
  for _, v := range tests1ice {
    w := v
    go func(w int) {
      fmt.Println(w)
    }(w)
  }
  time.Sleep(time.Second)
}
2
4
5
1
3

解决方案二:在调用gorouinte前将变量进行值拷贝

package main
import (
  "fmt"
  "time"
)
func main() {
  tests1ice := []int{1, 2, 3, 4, 5}
  for _, v := range tests1ice {
    w := v
    go func() {
      fmt.Println(w)
    }()
  }
  time.Sleep(time.Second)
}
1
3
2
5
4

Go的闭包看你犯错,Rust却默默帮你排坑


另外的例子:

package main
import (
  "fmt"
  "time"
)
func main() {
  s := []int{1, 2, 3}
  for _, v := range s {
    go func() {
      fmt.Println(v) // 输出结果3 3 3
    }()
  }
  time.Sleep(1e9)
}

无法得到预期结果1,2,3的原因是在没有将变量 v 的拷贝值传进匿名函数之前,只能获取最后一次循环的值,是新手最容易遇到的坑之一。有效规避方式为每次将变量v的拷贝传进函数:

package main
import (
  "fmt"
  "time"
)
func main() {
  s := []int{1, 2, 3}
  for _, v := range s {
    go func(v int) {
      fmt.Println(v) // 输出结果1,2,3或 1,3,2 或其他顺序
    }(v)
  }
  time.Sleep(1e9)
}

搭配defer使用:往defer里传入一个闭包,虽然是值传递,但是拷贝的是函数指针,可以解决一些使用defer会立刻拷贝函数中引用的外部参数引起的时机问题。

package main
import "fmt"
func main() {
  x, y := 1, 2
  defer func(a int) {
    fmt.Printf("x:%d,y:%d\n", a, y) // y 为闭包引用,最终结果为x:1,y:102
  }(x) // 复制 x 的值
  x += 100
  y += 100
}

无法得到期待的结果x:1,y:2的原因是:defer 调用会在当前函数执行结束前才被执行,这些调用被称为延迟调用,而defer 中使用匿名函数是一个闭包,y为闭包引用的外部变量会跟着闭包环境变化,当延迟调用时y已经变成102,所以最终输出的y也不再是2了。

有效规避方式只需要去掉defer即可


目录
相关文章
|
Go
Go 语言使用 goroutine 运行闭包的“坑”
Go 语言使用 goroutine 运行闭包的“坑”
57 0
|
Serverless Go
Go语言闭包不打烊,让你长见识!
Go语言闭包不打烊,让你长见识!
47 0
|
3月前
|
存储 运维 安全
go语言中闭包与匿名函数是什么?
本文探讨了Go语言中的匿名函数与闭包。首先介绍了匿名函数的定义与使用方式,包括直接调用、赋值给变量以及作为全局变量的应用。接着深入解析了闭包的概念及其本质,强调闭包能实现状态保持,但也警告其不当使用可能导致复杂的内存管理和运维问题。通过示例展示了如何利用闭包实现累加器功能,并对比了使用结构体字段的方法。最后,通过一个并发场景的示例说明了闭包在Go中处理多协程安全访问共享数据的应用,展示了闭包结合锁机制确保数据一致性的方式。
|
3月前
|
编译器 Go
Go语言中的闭包:封装数据与功能的强大工具
Go语言中的闭包:封装数据与功能的强大工具
|
5月前
|
Go
go的函数定义、递归、延迟、匿名、高阶、闭包
go的函数定义、递归、延迟、匿名、高阶、闭包
|
5月前
|
Go
Go语言进阶篇——浅谈函数中的闭包
Go语言进阶篇——浅谈函数中的闭包
|
6月前
|
Go C++
go 语言回调函数和闭包
go 语言回调函数和闭包
|
6月前
|
存储 编译器 Go
GO闭包实现原理(汇编级讲解)
函数闭包一点也不神秘,它就是函数和引用环境而组合的实体。在Go中,闭包在底层是一个结构体对象,它包含了函数指针与自由变量。Go编译器的逃逸分析机制,会将闭包对象分配至堆中,这样自由变量就不会随着函数栈的销毁而消失,它能依附着闭包实体而一直存在。因此,闭包使用的优缺点是很明显的:闭包能够避免使用全局变量,转而维持自由变量长期存储在内存之中;但是,这种隐式地持有自由变量,在使用不当时,会很容易造成内存浪费与泄露。附着闭包实体而一直存在。
79 0
GO闭包实现原理(汇编级讲解)
|
JavaScript 前端开发 Java
Go中的闭包、递归
Go中的闭包、递归
72 1
|
6天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
24 2