拥有函数名的函数只能在包级语法块中被声明,通过函数字面量,我们可绕过这一限制,在任何表达式中表示一个函数值。
函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数。
改写之前对strings.Map
的调用:
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
通过这种方式定义的函数可以访问完整的词法环境,这意味着在函数中定义的内部函数可以引用该函数的变量。
函数值不仅仅是一串代码,还记录了状态。
变量的生命周期不由它的作用域决定。
当匿名函数需要被递归调用时,我们必须首先声明一个变量(在上面的例子中,我们首先声明了visitAll
),再将匿名函数赋值给这个变量。如果不分成两步,函数字面量无法与visitAll
绑定,我们也无法递归调用该匿名函数。
append的参数“f(item)…”,会将f返回的一组元素一个个添加到worklist
中。
正确写法:
var rmdirs []func()
for _, d := range tempDirs() {dir := d // NOTE: necessary!os.MkdirAll(dir, 0755) // creates parent directories toormdirs = append(rmdirs, func() {os.RemoveAll(dir)})
}
// ...do some work…
for _, rmdir := range rmdirs {rmdir() // clean up
}
错误写法:
var rmdirs []func()
for _, dir := range tempDirs() {os.MkdirAll(dir, 0755)rmdirs = append(rmdirs, func() {os.RemoveAll(dir) // NOTE: incorrect!})
}
函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以dir
为例,后续的迭代会不断更新dir
的值,当删除操作执行时,for循环已完成,dir
中存储的值等于最后一次迭代的值。这意味着,每次对os.RemoveAll
的调用删除的都是相同的目录。
在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“…”,这表示该函数会接收任意数量的该类型参数。
func sum(vals ...int) int {total := 0for _, val := range vals {total += val}return total
}
如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符。下面的代码功能与上个例子中最后一条语句相同。
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"
interface{}表示函数的最后一个参数可以接收任意类型。
你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当执行到该条语句时,函数和参数表达式得到计算,但直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。
defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。
有点类似于java的finally,无论正常结束还是异常结束,都会执行defer语句,不过执行到该语句时就会去计算值,但是不执行。
func bigSlowOperation() {defer trace("bigSlowOperation")()time.Sleep(10 * time.Second)
}func trace(msg string) func() {start := time.Now()log.Printf("enter %s", msg)return func() {log.Printf("exit %s (%S)", msg, time.Since(start))}
}
如上,此时defer的对象是trace返回的匿名函数,所以在走到这一行事,会把return的func之前的所有函数计算并执行,等到退出时再执行这条返回的匿名函数。如果省略最后的()
,那么此时defer的就是trace本身,那么会等到退出时才会去执行打印enter
,并且返回的匿名函数是不会执行的。
对匿名函数采用defer机制,可以使其观察函数的返回值。
Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起painc异常。
一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine,日志信息中都会有与之相对的,发生panic时的函数调用堆栈跟踪信息。
不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用panic。比如,当程序到达了某条逻辑上不可能到达的路径。
为了方便诊断问题,runtime包允许程序员输出堆栈信息。
func main() {defer printStack()f(3)
}
func printStack() {var buf [4096]byten := runtime.Stack(buf[:], false)os.Stdout.Write(buf[:n])
}
如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。
考虑到语言解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。
func Parse(input string) (s *Syntax, err error) {defer func() {if p := recover(); p != nil {err = fmt.Errorf("internal error: %v", p)}}()// ...parser...
}
deferred函数帮助Parse从panic中恢复。在deferred函数内部,panic value被附加到错误信息中;并用err变量接收错误信息,返回给调用者。
为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为error处理,如果不是,则按照正常的panic进行处理。