函数
函数声明
-
每个函数声明都包含一个名字、一个形参列表、一个可选的返回列表以及函数体:
func name(parameter-list) (result-list) { body } -
形参列表指定了一组变量的参数名和参数类型,这些局部变量都由调用者提供的实参传递而来。
-
返回列表则指定了函数返回值的类型。
递归
- 函数可以递归调用,这意味着函数可以直接或间接地调用自己
- 许多编程语言使用固定长度的函数调用栈,递归的深度受限于固定长度的栈的大小,所以在进行深度递归调用的时候必须谨防栈溢出。但是Go语言实现了可变长度的栈,栈的大小会随着使用而增长,可达到1GB左右的上限
多返回值
-
一个函数能返回不止一个结果。
func findLinks(url string) ([]string, error) { // 内容 } -
调用一个涉及多值计算的函数会返回一组值。如果调用者要使用这些返回值,必须显式的将返回值赋给变量
links, err := findLinks(url)
错误
- 当函数调用发送错误时返回一个附加的结果作为错误值,习惯上将错误值作为最后一个结果返回。如果错误只有一种情况,结果通常设置为布尔类型
- 更多时候,尤其是对于I/O操作,错误的原因可能多种多样,而调用者则需要一些详细的信息,这种情况下,错误的结果类型往往是error
- 一个错误可能是空值或者非空值。空值意味着成功而非空值意味着失败,而且非空的错误类型有一个错误消息字符串,可以通过调用它的Error方法或者fmt.Println(err)或fmt.Printf("%v", err)直接输出错误消息
错误处理策略
-
当一个函数调用返回一个错误时,调用者应当负责检查错误并采取合适的处理应对。
-
- 最常见的就是将错误传递下去
resp, err := http.Get(url) if err != nil { return nil, err } -
构建一个新的错误消息
if err != nil { return nil, fmt.Errorf("parsing %s as HTML: %v", url, err) }我们为原始的错误消息不断地添加额外的上下文信息来建立一个可读的错误描述。当错误最终被程序的main函数处理时,它应当能提供一个从根本问题到总体故障的清晰因果链
-
- 对于不固定或者不可预测的错误,在短暂的间隔后对操作进行重试是合乎情理的,超出一定的重试次数和限定时间后再报错退出
func WaitForServer(url string) error { const timeout = 1 * time.Minute deadline := time.Now().Add(timeout) for trier := 0; time.Now().Before(deadline); trie++ { _, err := http.Head(url) if err == nil { return nil // 成功 } log.Printf("server not responding (%s); retrying...", err) time.Sleep(time.Second << uint(tries)) // 指数退避策略 } return fmt.Errorf("server %s failed to respond after %s", url, timeout) } -
- 如果依旧不能顺利进行下去,调用者能够输出错误然后优雅的停止程序,但是一般这样的处理应该留给主程序部分。通常库函数应当将错误传递给调用者,除非这个错误表示一个内部一致性的错误,这意味着库内部存在bug
-
- 在一些错误情况下,只记录下错误信息然后程序继续运行。同样地,可以选择使用
log包来增加日志的常用前缀,并且将日期和时间略去。
if err := Ping(); err != nil { log.Printf("ping failed: %v; networking disabled\n", err) } - 在一些错误情况下,只记录下错误信息然后程序继续运行。同样地,可以选择使用
-
- 在某些罕见的情况下我们可以直接安全地忽略掉整个日志
dir, err := ioutill.TempDir("", "scratch") if err != nil { return fmt.Errorf("failed to create temp dir: %v", err) } // ...使用临时目录... os.RemoveAll(dir) // 忽略错误,$TMPDIR会被周期性的删除这里,调用
os.RemoveAll可能会失败,但程序忽略了这个错误,我们有意地抛弃了这个错误,但程序的逻辑看上去就和我们忘了处理一样。要习惯考虑到每个函数调用可能发生的出错情况,当有意的忽略某个错误的时候,清楚的注释一下你的意图。
-
文件结束标识
-
对于读取文件时读取到文件结束引起的错误,始终都将会得到一个与众不同的错误————
io.EOF,package io import "errors" // 当没有更多输入时,将会返回EOF var EOF = error.New("EOF") -
调用者可以使用一个简单的比较操作来检查这种情况,在下面的循环中,不断从标准输入中读取字符
in := bufio.NewReader(os.Stdin) for { r, _, err := in.ReadRune() if err == io.EOF { bread // 读取结束 } if err != nil { return fmt.Errorf("read failed: %v", err) } // ...使用r... }
函数变量
-
在Go中函数是一等公民,和其他值一样,函数变量也有类型,而且它们可以赋给变量或者传递或者从其他函数中返回。函数变量可以像其他函数一样调用,例如:
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 // f现在是一个函数变量 fmt.Println(f(3)) // "9" f = negative fmt.Println(f(3)) // "-3" fmt.Printf("%T\n", f) // "func(int) int" f = product // 编译错误:不能把类型func(int, int) int 赋值给 func(int) int -
函数类型的零值是nil(空值),调用一个空的函数变量将导致宕机
var f func(int) int f(3) // 宕机:调用空函数 -
函数变量可以和空值相比较:
var f func(int) int if f != nil { f(3) } -
但是函数变量本身不可比较,所以不可以互相进行比较或者作为键值出现在map中
-
函数变量使得函数不仅将数据进行参数化,还将函数的行为当作参数进行传递,比如标准库中的
strings.Map方法:func Map(mapping func(rune) rune, s string) string { ... }该方法的第一个形参是一个函数,可以按如下的方式调用
func add1(r rune) rune { return r + 1 } fmt.Println(strings.Map(add1, "HAL-9000")) // "IBM.:111" // 相当于把第二个参数按照第一个参数传入的函数的行为执行了一次
匿名函数
-
命名函数只能在包级别的作用域声明,但是我们可以使用函数字面量在任何表达式内指定函数变量
-
函数字面量就像函数声明,但在func关键字后面没有函数的名称。他的一个表达式,他的值称作匿名函数
strings.Map(func(r rune) rune { return r + 1}, "HAL-9000") -
更重要的是,以这种方式定义的函数可以获取到整个词法环境