匿名函数
当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }。 函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function)。匿名函数由一个不带函数名的函数声明和函数体组成.通常不希望再次使用(即只使用一次的)的函数可以定义为匿名函数.
匿名函数结构:
func() {
//func body
}() //花括号后加()表示函数调用,此处声明时为指定参数列表,
如:
fun(a,b int) {
fmt.Println(a+b)
}(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的情况如:
f := make(chan func() string, 2)
f <- func() string { return "Hello, World!" }
fmt.Println((<-f)())
在谈到匿名函数我们在补充下闭包函数,闭包是函数式语言中的概念,没有研究过函数式语言的用户可能很难理解闭包的强大,相关的概念超出了本书的范围。Go语言是支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的。 匿名函数是无需定义标示符(函数名)的函数;而闭包是指能够访问自由变量的函数。换句话说,定义在闭包中的函数可以”记忆”它被创建时候的环境。闭包函数=匿名函数+环境变量。
func f(i int) func() int {
return func() int {
i++
return i
}
}
运行的结果:
c1 := f(0)
c2 := f(0)
c1() // reference to i, i = 0, return 1
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的栈上。
接着我们继续分析逃逸分析:
func agency() *Cursor {
var c Cursor
c.X = 500
noinline()
return &c
}
Cursor是一个结构体(这种写法在C语言中是不允许的,因为变量c是在栈上分配的,当函数agency返回后c的空间就失效了)。但是,在Go语言规范中有说明,这种写法在Go语言中合法的。语言会自动地识别出这种情况并在堆上分配c的内存,而不是函数agency的栈上。
为了验证这一点,可以观察函数agency生成的汇编代码:
MOVQ $type."".Cursor+0(SB),(SP) // 取变量c的类型,也就是Cursor
PCDATA $0,$16
PCDATA $1,$0
CALL ,runtime.new(SB) // 调用new函数,相当于new(Cursor)
PCDATA $0,$-1
MOVQ 8(SP),AX // 取c.X的地址放到AX寄存器
MOVQ $500,(AX) // 将AX存放的内存地址的值赋为500
MOVQ AX,"".~r0+24(FP)
ADDQ $16,SP
识别出变量需要在堆上分配,是由编译器的一种叫escape analyze的技术实现的。如果输入命令:
go build --gcflags=-m main.go
输出结果:
./main.go:20: moved to heap: c
./main.go:23: &c escapes to heap
表示c逃逸了,被移到堆中。escape analyze可以分析出变量的作用范围,这是对垃圾回收很重要的一项技术。
我们在回到闭包结构中,前面说过,闭包是函数和它所引用的环境。
type Closure struct {
F func()()
i *int
}
事实上,Go在底层确实就是这样表示一个闭包的。让我们看一下汇编代码:
func f(i int) func() int {
return func() int {
i++
return i
}
}
MOVQ $type.int+0(SB),(SP)
PCDATA $0,$16
PCDATA $1,$0
CALL ,runtime.new(SB) // 是不是很熟悉,这一段就是i = new(int)
...
MOVQ $type.struct { F uintptr; A0 *int }+0(SB),(SP) // 这个结构体就是闭包的类型
...
CALL ,runtime.new(SB) // 接下来相当于 new(Closure)
PCDATA $0,$-1
MOVQ 8(SP),AX
NOP ,
MOVQ $"".func·001+0(SB),BP
MOVQ BP,(AX) // 函数地址赋值给Closure的F部分
NOP ,
MOVQ "".&i+16(SP),BP // 将堆中new的变量i的地址赋值给Closure的值部分
MOVQ BP,8(AX)
MOVQ AX,"".~r1+40(FP)
ADDQ $24,SP
RET ,
其中func·001是另一个函数的函数地址,也就是agency返回的那个函数。
goroutine是常见的匿名函数,通常我们会使用关键字 go 启动了一个匿名函数作为 goroutine。 使用匿名函数或闭包创建 goroutine 时,除了将函数定义部分写在 go 的后面之外,还需要加上匿名函数的调用参数,格式如下:
go func( 参数列表 ){
函数体
}( 调用参数列表 )
其中:
- 参数列表:函数体内的参数变量列表。
- 函数体:匿名函数的代码。
- 调用参数列表:启动 goroutine 时,需要向匿名函数传递的调用参数。
var x int64 = 20
var y int64 = 10
var wg sync.WaitGroup
wg.Add(1)
//定义一个匿名函数,并对该函数开启协程
go func(x, y int64) {
z := x+y
fmt.Println("the reuslt value:",z)
wg.Done()
}(x,y)
//由于这个函数是匿名函数,所以调用方式就直接是(x,y)去调用,不用输入函数名。
wg.Wait()
匿名函数也可以接受声明时指定的参数。我们指定匿名函数要接受两个参数:x,y都是int64的类型。
Goroutine是异步执行的,有的时候为了防止在结束mian函数的时候结束掉Goroutine,所以需要同步等待,这个时候就需要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它会等待它收集的所有 goroutine 任务全部完成。在WaitGroup里主要有三个方法:
Add, 可以添加或减少 goroutine的数量. Done, 相当于Add(-1). Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0.