对于任何语言而言,函数都是结构化编程中的重要一环,通过函数机制,可以把一个大的模块分解成多个小任务,让代码结构更清晰,可复用性大大提高。
本文将介绍如何定义,调用函数,如何定义和接收函数错误,以及 golang 特有的可变参数,闭包,defer,panic 和 recover 等机制。
Go 语言的函数基本组成为:函数名,入参列表,函数体,返回参数列表。
func function_name( [parameter list] ) [return_types] {
// 函数体
}
// 如:
// func find_max(num1, num2 int) int {}
// func swap(x, y string) (string, string) {}
如果一个函数所有的返回值都有显式的变量名,那么该函数的return语句可以省略操作数。这称之为 bare return,但是不建议使用这种方法,会导致程序的可读性变差。
func CountWordsAndImages(url string) (words, images int, err error) {
// 函数体
return // 等价于 return words, images, err
}
words, images, err := CountWordsAndImages("/test/test.go")
//...
注意:
_
接收。参数数量可变的函数称为为可变参数函数。例如 fmt.Printf()
函数,首先接收一个必备的参数,之后接收任意个数的后续参数。
func Printf(format string, a ...interface{
}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
在声明可变参数函数时,只能把可变参数作为参数列表的最后一个参数,在参数类型之前加上省略符号 ...
,这表示该函数会接收任意数量的该类型参数。如:
func sum(vals...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
fmt.Println(sum()) // 0
fmt.Println(sum(3)) // 3
fmt.Println(sum(1, 2, 3, 4)) // 10
fmt.Printf("%T", sum) // func(...int)
实际上,可变参数会被处理为一个slice传递给函数,如果原始参数本身就是一个 slice,则需要使用 ...
先将切片展开,然后再作为参数传递,如:
values := []int{
1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"
内置的 error 类型是接口类型,可能是 nil 或 non-nil,nil 意味着函数运行成功,non-nil 代表运行失败。对于non-nil的error类型, 我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。golang 推荐在任何有可能出现错误的函数中都添加 error 回参来增加程序的鲁棒性,比如 db 读写,接口调用等。
错误相关的方法都为于 errors 包中,在 go 语言的编码风格中,为了程序逻辑的连贯性,通常会将处理失败的逻辑代码放在处理成功的代码之前,并且不需要将正常逻辑放在 else 中。
func f() error {
}
err := f()
if err != nil {
// 错误处理
}
// 正常逻辑
当函数出现错误时,通常在子程序中将错误返回给调用层并打印日志,调用层再进行相应的处理。也可以在发生错误时采取重试,回滚等操作。
使用函数字面量(function literal)可以在任何表达式中声明一个函数并使用,这种方法声明的函数没有函数名,称为匿名函数(anonymous function),通常在 defer、go 等情况使用,如:
defer func() {
// do something...
}()
在一个函数中定义内部匿名函数时,匿名函数可以访问该函数完整的词法环境 (lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变,如:
// squares 函数的入参为空,返回值为一个匿名函数,该匿名函数的返回值为 int
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
g := squares()
fmt.Println(g()) // 1
fmt.Println(g()) // 4
fmt.Println(g()) // 9
}
每次调用 squares 会生成一个局部变量 x 并返回一个匿名函数,每次调用该匿名函数时,函数都会使 x 的值加 1,这意味着匿名内部函数可以引用并修改 squares 的局部变量,也就是说匿名函数和squares中,存在变量引用,这种方式称为闭包。
通过这个例子,可以看出变量的生命周期不由作用域决定,squares 返回后,x 仍隐式的存在于 f 中。
在某些需要释放资源的函数中(如关闭文件,关闭 db,处理互斥锁等),为了保证资源被释放,需要在每一个逻辑中进行处理,如下伪代码:
func demo() {
file, err := os.Open(filename)
if err != nil {
return
}
操作。。。
if err1 != nil {
file.Close()
return
}
继续操作。。。
if err2 != nil {
file.Close()
return
}
正常结束。。。
file.Close()
return
}
为了确保在所有执行路径下(即使函数运行失败)都释放了资源,在所有的节点都要进行 file.Close()
操作,随着函数变得复杂,需要处理的错误也变多,维护清理逻辑变得越来越困难。而 Go 语言独有的 defer 机制可以很好的解决这个问题。
defer 语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁,释放资源的defer应该直接跟在请求资源的语句后。通过 defer 机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。
当defer语句被执行时,跟在defer后面的函数会被延迟执行,直到包含该 defer 语句的函数执行完毕时,defer 的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。
使用 defer 修改后的示例如下:
func demo() {
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close()
操作。。。
if err1 != nil {
return
}
继续操作。。。
if err2 != nil {
return
}
正常结束。。。
return
}
可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反,先 defer 的函数后执行,类似一个栈。
要注意的是尽量不要在循环体中直接加入 defer,否则在所有文件都被处理完之后,defer 才会被执行,在这个过程中,不会有任何一个文件被成功关闭,可能导致系统的文件描述符被耗尽。如:
for _, filename := range filenames {
f, err := os.Open(filename)
if err ! = nil {
return err
}
defer f.Close() // 风险操作,可能导致文件描述符耗尽
}
正确的做法是将循环体中的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() // defer 在每一次 doFile 执行完毕后都会运行
}
需要注意的是 defer 后面跟着的应该是一个函数调用,而不是函数声明,所以要特别注意括号的使用,尤其是在 defer 中使用匿名函数时,如:
func add(x, y int) int {
defer func() {
fmt.Println("defer")
}() // 这个括号不能忘记
return x + y
}
有些错误在编译时无法被发现,只会在运行时引发,如数组越界、空指针引用等。这些运行时错误会引起 painc 异常,中断程序并且立即执行该 goroutine 中的 defer 函数,随后,程序崩溃并输出日志信息,包括 panic value 和函数调用的堆栈跟踪信息。
也可以手动调用内置的panic函数引发panic异常,如:
if err != nil {
panic(fmt.Printf("error: %s", err))
}
但是要注意,由于 panic 会引起程序的崩溃,所以除非是非常严重的错误,否则尽量避免使用 Panic 。对于大部分漏洞,我们应该使用Go提供的错误机制来处理异常,而不是panic,以避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的 I/O 操作都应该被优雅的处理
使用 recover 可以帮助程序从 panic 异常中恢复。或者至少在程序崩溃前做一些处理,例如当web服务器遇到不可预料的严重问题时,在崩溃前将所有的连接关闭,以免客户端一直处于等待状态。
如果在 defer 函数中调用了内置函数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...
}
defer 函数帮助 Parse 从 panic 中恢复。在 defer 函数内部,panic value 被附加到错误信息中,并用 err 变量接收错误信息,返回给调用者。
然而,在实际使用中不推荐使用 recover 函数,否则很有可能不能及时的结束已经出错的函数,并导致更加严重的后果。