匿名函数

当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }。 函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function)。匿名函数由一个不带函数名的函数声明和函数体组成.通常不希望再次使用(即只使用一次的)的函数可以定义为匿名函数.

  1. 匿名函数结构:
  2. func() {
  3. //func body
  4. }() //花括号后加()表示函数调用,此处声明时为指定参数列表,
  5. 如:
  6. fun(a,b int) {
  7. fmt.Println(a+b)
  8. }(1,2)

表示参数列表的第一对括号必须紧挨着关键字 func,因为匿名函数没有名称。花括号 {} 涵盖着函数体,最后的一对括号表示对该匿名函数的调用。

这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body),但可以被赋值于某个变量,即保存函数的地址到变量中:fn := func(x, y int) int { return x + y },然后通过变量名对函数进行调用:fn(1,2)。

除此之外,也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)。

通常也会有使用匿名函数channel的情况如:

  1. f := make(chan func() string, 2)
  2. f <- func() string { return "Hello, World!" }
  3. fmt.Println((<-f)())

在谈到匿名函数我们在补充下闭包函数,闭包是函数式语言中的概念,没有研究过函数式语言的用户可能很难理解闭包的强大,相关的概念超出了本书的范围。Go语言是支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的。 匿名函数是无需定义标示符(函数名)的函数;而闭包是指能够访问自由变量的函数。换句话说,定义在闭包中的函数可以”记忆”它被创建时候的环境。闭包函数=匿名函数+环境变量。

  1. func f(i int) func() int {
  2. return func() int {
  3. i++
  4. return i
  5. }
  6. }

运行的结果:

  1. c1 := f(0)
  2. c2 := f(0)
  3. c1() // reference to i, i = 0, return 1
  4. c2() // reference to another i, i = 0, return 1

函数f返回了一个函数,返回的这个函数,返回的这个函数就是一个闭包。这个函数中本身是没有定义变量i的,而是引用了它所在的环境(函数f)中的变量i。

c1跟c2引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数f每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。 变量i是函数f中的局部变量,假设这个变量是在函数f的栈中分配的,是不可以的。因为函数f返回以后,对应的栈就失效了,f返回的那个函数中变量i就引用一个失效的位置了。所以闭包的环境中引用的变量不能够在栈上分配。Go编译器通过逃逸分析自动识别出变量的作用域,在堆上分配内存,而不是在函数f的栈上。

接着我们继续分析逃逸分析:

  1. func agency() *Cursor {
  2. var c Cursor
  3. c.X = 500
  4. noinline()
  5. return &c
  6. }

Cursor是一个结构体(这种写法在C语言中是不允许的,因为变量c是在栈上分配的,当函数agency返回后c的空间就失效了)。但是,在Go语言规范中有说明,这种写法在Go语言中合法的。语言会自动地识别出这种情况并在堆上分配c的内存,而不是函数agency的栈上。

为了验证这一点,可以观察函数agency生成的汇编代码:

  1. MOVQ $type."".Cursor+0(SB),(SP) // 取变量c的类型,也就是Cursor
  2. PCDATA $0,$16
  3. PCDATA $1,$0
  4. CALL ,runtime.new(SB) // 调用new函数,相当于new(Cursor)
  5. PCDATA $0,$-1
  6. MOVQ 8(SP),AX // 取c.X的地址放到AX寄存器
  7. MOVQ $500,(AX) // 将AX存放的内存地址的值赋为500
  8. MOVQ AX,"".~r0+24(FP)
  9. ADDQ $16,SP

识别出变量需要在堆上分配,是由编译器的一种叫escape analyze的技术实现的。如果输入命令:

  1. go build --gcflags=-m main.go

输出结果:

  1. ./main.go:20: moved to heap: c
  2. ./main.go:23: &c escapes to heap

表示c逃逸了,被移到堆中。escape analyze可以分析出变量的作用范围,这是对垃圾回收很重要的一项技术。

我们在回到闭包结构中,前面说过,闭包是函数和它所引用的环境。

  1. type Closure struct {
  2. F func()()
  3. i *int
  4. }

事实上,Go在底层确实就是这样表示一个闭包的。让我们看一下汇编代码:

  1. func f(i int) func() int {
  2. return func() int {
  3. i++
  4. return i
  5. }
  6. }
  7. MOVQ $type.int+0(SB),(SP)
  8. PCDATA $0,$16
  9. PCDATA $1,$0
  10. CALL ,runtime.new(SB) // 是不是很熟悉,这一段就是i = new(int)
  11. ...
  12. MOVQ $type.struct { F uintptr; A0 *int }+0(SB),(SP) // 这个结构体就是闭包的类型
  13. ...
  14. CALL ,runtime.new(SB) // 接下来相当于 new(Closure)
  15. PCDATA $0,$-1
  16. MOVQ 8(SP),AX
  17. NOP ,
  18. MOVQ $"".func·001+0(SB),BP
  19. MOVQ BP,(AX) // 函数地址赋值给Closure的F部分
  20. NOP ,
  21. MOVQ "".&i+16(SP),BP // 将堆中new的变量i的地址赋值给Closure的值部分
  22. MOVQ BP,8(AX)
  23. MOVQ AX,"".~r1+40(FP)
  24. ADDQ $24,SP
  25. RET ,

其中func·001是另一个函数的函数地址,也就是agency返回的那个函数。

goroutine是常见的匿名函数,通常我们会使用关键字 go 启动了一个匿名函数作为 goroutine。 使用匿名函数或闭包创建 goroutine 时,除了将函数定义部分写在 go 的后面之外,还需要加上匿名函数的调用参数,格式如下:

  1. go func( 参数列表 ){
  2. 函数体
  3. }( 调用参数列表 )

其中:

  • 参数列表:函数体内的参数变量列表。
  • 函数体:匿名函数的代码。
  • 调用参数列表:启动 goroutine 时,需要向匿名函数传递的调用参数。
  1. var x int64 = 20
  2. var y int64 = 10
  3. var wg sync.WaitGroup
  4. wg.Add(1)
  5. //定义一个匿名函数,并对该函数开启协程
  6. go func(x, y int64) {
  7. z := x+y
  8. fmt.Println("the reuslt value:",z)
  9. wg.Done()
  10. }(x,y)
  11. //由于这个函数是匿名函数,所以调用方式就直接是(x,y)去调用,不用输入函数名。
  12. wg.Wait()

匿名函数也可以接受声明时指定的参数。我们指定匿名函数要接受两个参数:x,y都是int64的类型。

Goroutine是异步执行的,有的时候为了防止在结束mian函数的时候结束掉Goroutine,所以需要同步等待,这个时候就需要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它会等待它收集的所有 goroutine 任务全部完成。在WaitGroup里主要有三个方法:

Add, 可以添加或减少 goroutine的数量. Done, 相当于Add(-1). Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0.