让我们演示一个文件复制的例子:函数需要打开两个文件,然后将其中一个文件的内容复制到另一个文件:
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}
上面的代码虽然能够工作,但是隐藏一个bug。如果第一个os.Open
调用成功,但是第二个os.Create
调用失败,那么会在没有释放src
文件资源的情况下返回。虽然我们可以通过在第二个返回语句前添加src.Close()
调用来修复这个BUG;但是当代码变得复杂时,类似的问题将很难被发现和修复。我们可以通过defer
语句来确保每个被正常打开的文件都能被正常关闭:
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
defer
语句可以让我们在打开文件时马上思考如何关闭文件。不管函数如何返回,文件关闭语句始终会被执行。同时defer
语句可以保证,即使io.Copy
发生了异常,文件依然可以安全地关闭。
前文我们说到,Go语言中的导出函数一般不抛出异常,一个未受控的异常可以看作是程序的BUG。但是对于那些提供类似Web服务的框架而言;它们经常需要接入第三方的中间件。因为第三方的中间件是否存在BUG是否会抛出异常,Web框架本身是不能确定的。为了提高系统的稳定性,Web框架一般会通过recover
来防御性地捕获所有处理流程中可能产生的异常,然后将异常转为普通的错误返回。
让我们以JSON解析器为例,说明recover的使用场景。考虑到JSON解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。
func ParseJSON(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("JSON: internal error: %v", p)
}
}()
// ...parser...
}
标准库中的json
包,在内部递归解析JSON数据的时候如果遇到错误,会通过抛出异常的方式来快速跳出深度嵌套的函数调用,然后由最外一级的接口通过recover
捕获panic
,然后返回相应的错误信息。
Go语言库的实现习惯: 即使在包内部使用了panic
,但是在导出函数时会被转化为明确的错误值。