函数

函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。

函数的类型被称为函数的签名,如果有两个函数形式参数列表和返回值列表中的变量类型一一对应,那么两个函数被认为有相同的类型或签名,形参和返回值的变量名不影响函数签名,也不影响他们是否可以以省略参数类型的形式标识。

函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。实参通过值的方式传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是实参包括引用类型:指针、slice、map、function、cahnnel 等类型,实参可能由于函数的间接引用被修改。

大部分编程语言使用固定大小的函数调用栈,常见的大小从 64KB 到 2MB 不等。固定大小栈会限制递归的深度,当你用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。与此相反,Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。

函数值

在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值(function value)的调用类似函数调用,内置函数都在 builtinunsafe 声明

func square(n int) int { return n * n }
func negative(n int) int { return -n }
func product(m, n int) int { return m * n }

f := square
fmt.Println(f(3)) // "9"

f = negative
fmt.Println(f(3))     // "-3"
fmt.Printf("%T\n", f) // "func(int) int"

f = product // compile error: can't assign func(int, int) int to func(int) int

函数值的零值是 nil,调用值为 nil 的函数值会 panic,函数值可以与 nil 进行比较,但是函数值之间是不可比较的,所以不能当做 map 中的 key

匿名函数

在函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量,变量引用(指针)

拥有函数名的函数只能在包级别语法块中被声明,通过函数字面量(function literal)可以绕过这一个限制,在任何表达式中表示一个函数值。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function)。

函数字面量允许我们在使用函数时再定义它,并且定义的函数可以访问完整的词法环境,这意味着在函数中定义的内部函数可以引用该函数的变量

package main

import "fmt"

// https://play.golang.org/p/ItXWeRANmkT
// squares返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。
func squares() func() int {
	var x int
	return func() int {
		x++
		return x * x
	}
}

func main() {
	f := squares()
	fmt.Println(f()) // "1"
	fmt.Println(f()) // "4"
	fmt.Println(f()) // "9"
	fmt.Println(f()) // "16"
}

函数值不仅仅是一串代码,还记录了状态,在 squares 中定义的匿名内部函数可以访问和更新 squares 中局部变量,这意味着匿名函数和 squares 存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。Go 使用闭包(closures)技术实现函数值,也把函数值叫做闭包。

并且这个例子可以看出,变量的声明周期不由它的作用域决定: squares 返回后,变量 x 仍然隐式的存在 f 中。

当匿名函数需要被递归调用时,必须先声明一个变量,在将匿名函数赋值给这个变量,如果不分成两步则无法递归调用该匿名函数。

func Do(m map[string][]string) []string {
	...
	var visitAll func(items []string)
	visitAll = func(items []string) {
		for _, item := range items {
			visitAll(m[item])
		}
	}
	...
	return nil
}

闭包

闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。

这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例

定义在一个函数内部的函数

陷阱

Golang 词法作用域的一个陷阱

为什么输出都是相同的值,并且都是 list 最后的值?原因在于循环变量的作用域,for 循环语句中引入了新的词法块(词法块就是 {} 大括号),循环变量 k 和 v 在词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值,以 v 为例,后续迭代会不断更新 v 的值,当运行协程时 for 已经循环完成,v 中存储的值等于最后一次迭代的值,所以输出都是相同的结果。这个问题不仅存在 range 中,普通的 for 也会存在这个问题

为了解决这个问题,可以引入与循环变量同名的局部变量,作为循环变量的副本

package main

import (
	"fmt"
	"sync"
)

// https://play.golang.org/p/nA4xecc0hRi
func main() {

	list := [...]int{1, 2, 3, 4, 5}
	var wg sync.WaitGroup

	for k, v := range list {
		wg.Add(1)

		// 同名的局部变量
		k, v := k, v
		go func() {
			fmt.Println(k, v)
			wg.Done()
		}()
	}

	wg.Wait()
}

// output:
// 4 5
// 0 1
// 1 2
// 2 3
// 3 4

或者把 k 和 v 当成变量输入

// https://play.golang.org/p/A1yYnjdu1t2
for k, v := range list {
    wg.Add(1)

    go func(k, v int) {
        fmt.Println(k, v)
        wg.Done()
    }(k, v)
}

defer

LIFO 先进后出

defer语句中的函数会在 return 语句更新返回值变量后再执行,又因为在函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量,所以,对匿名函数采用 defer 机制,可以使其观察函数的返回值。被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值。

package main

import (
	"fmt"
)

// https://play.golang.org/p/VJpwMJfEy_q
func main() {
	fmt.Println("triple", triple(4))
	fmt.Println("double", double(4))
}

func triple(x int) (result int) {
	// 在函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量
	// 匿名函数甚至可以修改函数返回给调用者的返回值
	defer func() { result += x }()
	return double(x)
}

func double(x int) (result int) {
	defer func() { fmt.Printf("double(%d) = %d\n", x, result) }()
	return x + x
}

// output:
// double(4) = 8
// triple 12
// double(4) = 8
// double 8

参考资料