目录
第五章 函数
函数声明
- 没有默认参数值
- 实参通过值传递,形参是实参的拷贝
- 实参是引用类型(指针,slice,map,function,channel等)时,实参可能会被修改
- 可能会遇到没有函数体的函数声明,这表示该函数不是Go实现的,例如
package math
func Sin(x float64) float //implemented in assembly language
递归
- 大部分编程语言使用固定大小的函数调用栈,常见打下从64KB到2MB不等。但是,Go采用可变栈,这使得我们使用递归时不必考虑溢出和安全问题
多返回值
错误
- 在Go的错误处理中,程序运行失败时会返回错误信息,是预期的结果而非异常
- 当错误原因只有一个,用一个布尔值,通常被命名为ok就可以了
- 当错误原因有很多个,用户需要了解更多错误信息,会用到error类型
- 内置的error是接口类型,它的值为nil或non-nil
错误处理策略
- 传播错误:函数中某个子程序失败,会变成该函数的失败
1.可以使用fmt.Errorf格式化错误信息
2.错误信息经常以链式组合在一起,故错误信息中应避免大写和换行符
3.编写错误信息时,要确保错误信息对问题细节的描述是详尽的 - 偶然性错误:如果错误发生是偶然性的,或不可预知的问题导致的
1.重新尝试失败操作(限制重试的时间间隔或重试的次数,防止无限制的重试) - 无法运行错误:如果错误发生后,程序无法继续运行
1.输出错误信息并结束程序(这种策略只在main中执行)
2.对库函数而言,应仅向上传播错误
3.如果错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序
// (In function main.)
if err := WaitForServer(url); err != nil {
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
os.Exit(1)
}
4.或者更简洁的写法(与上例等价)
if err := WaitForServer(url); err != nil {
log.Fatalf("Site is down: %v\n", err)
}
- 无影响的错误:只需要输出错误信息就够了,不需要中断程序的运行
//log包中的所有函数会为没有换行符的字符串增加换行符
if err := Ping(); err != nil {
log.Printf("ping failed: %v; networking disabled",err)
}
if err := Ping(); err != nil {
fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
}
- 可忽略错误:我们可以直接忽略掉错误
dir, err := ioutil.TempDir("", "scratch")
if err != nil {
return fmt.Errorf("failed to create temp dir: %v",err)
}
// ...use temp dir...
os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically
- 错误处理推荐风格:
1.检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前。
2.如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在else语句块中,而应直接放在函数体中
3.换句话说,先进行一系列的初始检查,防止错误发生,之后是函数的实际逻辑
文件结尾错误(EOF)
- io包保证任何由文件结束引起的读取失败都返回同一个错误:io.EOF
package io
import "errors"
// EOF is the error returned by Read when no more input is available.
var EOF = errors.New("EOF")
函数值
- 在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其它变量,传递给函数,从函数返回
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
var f func(int) int
f(3) // 此处f的值为nil, 会引起panic错误
//还可以和nil比较,但函数值之间不可比较
if f != nil {
f(3)
}
匿名函数(anonymous function)
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
// 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中的返回值func()可以访问和更新x,这意味着匿名函数和squares中,存在变量引用(函数值属于引用类型和函数不可比较的原因)
- go使用闭包(closures)技术实现函数值,通常也把函数值称为闭包
- 当匿名函数需要递归时,需要声明一个变量
func main() {
for i, course := range topoSort(prereqs) {
fmt.Printf("%d:\t%s\n", i+1, course)
}
}
func topoSort(m map[string][]string) []string {
var order []string
seen := make(map[string]bool)
var visitAll func(items []string)
visitAll = func(items []string) {
for _, item := range items {
if !seen[item] {
seen[item] = true
visitAll(m[item])
order = append(order, item)
}
}
}
var keys []string
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
visitAll(keys)
return order
}
警告:捕获迭代变量
- 可行的做法:
var rmdirs []func()
for _, d := range tempDirs() {
dir := d // NOTE: necessary!
os.MkdirAll(dir, 0755) // creates parent directories too
rmdirs = 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!
})
}
- 问题的原因在于循环变量的作用域。
1.for循环引入了新的词法块,循环变量dir在这个词法块中被声明
2.在该循环中生成的所有函数值都共享相同的循环变量
3.函数值记录的是循环变量的内存地址,而不是循环变量某一时刻的值
可变参数
- 声明可变参数时需要加上...
func sum(vals...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
func main() {
fmt.Println(sum()) // "0"
fmt.Println(sum(3)) // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"
}
- 显然,上面例子的vals..int被当作[]int处理了
- sum(1,2,3,4)隐式创建了一个数组,将数组作为参数切片,传给了sum
Deferred函数
- 使用defer关键字,跟在其后的函数将被延迟执行:
1.直到包含该defer语句的函数执行完毕,defer语句后的函数才被执行,
2.无论再此之前是否执行了return或者panic
3.可以在一个函数中执行多条defer语句,执行顺序和他们的声明顺序相反
4.在循环体中的defer语句,只有在函数执行完毕后,延迟的函数才被执行
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // NOTE: risky; could run out of file
descriptors
// ...process f...
}
5.解决的方法是将defer移动到另外的函数
for _, filename := range filenames {
if err := doFile(filename); err != nil {
return err
}
}
func doFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
// ...process f...
}
Panic异常
- 运行时检查,引起的错误就是panic异常
- 一般而言,panic发生后程序会中断运行,并立即执行该goroutine中的defer,之后,程序崩溃并输出日志信息
- 日志信息包括panic value和函数调用的堆栈跟踪信息
- 可以人为引发panic(只在非常严重的错误下使用):使用内置的panic函数
- runtime包允许程序员输出堆栈信息
gopl.io/ch5/defer2
func main() {
defer printStack()
f(3)
}
func printStack() {
var buf [4096]byte
n := runtime.Stack(buf[:], false)
os.Stdout.Write(buf[:n])
}
Recover捕获异常
- “举个例子,当web服务器遇到不可预料的严重问题时,在崩
溃前应该将所有的连接关闭;如果不做任何处理,会使得客户端一直处于等待状态。如果web
服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。” - "如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异
常,recover会使程序从panic中恢复,并返回panic value" - "导致panic异常的函数不会继续运
行,但能正常返回。在未发生panic时调用recover,recover会返回nil"
func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...parser...
}