案例
首先,我们看一个拷贝文件函数的示例。
func CopyFile(srcName, dstName string) error {
src, err := os.Open(srcName) //①
if err != nil {
return err
}
stat, err := src.Stat()
if err != nil {
src.Close() //注意关闭1
return err
}
if stat.IsDir() { //②
src.Close() //注意关闭2
return fmt.Errorf("file %q is a directory", srcName)
}
dst, err := os.Create(dstName) //③
if err != nil {
src.Close() //注意关闭3
return err
}
_, err = io.Copy(dst, src) //④
if err != nil {
src.Close() //注意关闭4
dst.Close() //注意关闭5
return err
}
err = dst.Sync() //⑤
src.Close() //注意关闭6
dst.Close() //注意关闭7
return err
}
① 打开源文件
② 检查是否是目录
③ 创建目标文件
④ 拷贝源文件到目标文件
⑤ 刷新文件系统缓冲区
我们需要管理该文件描述符的关闭,因为一个 *os.File一旦被打开准备读写后,它就必须要使用Close函数进行关闭。
但值得注意的是,上面一共需要调用7处Close方法,如果有一处遗漏,就将导致错误发生。
解决方法
Go提供了defer关键字,可以优雅的解决该种问题。
在函数返回的时候会调用defer函数。
即使在主函数panic或意外终止时defer函数也能保证被执行。defer语句会被推送到栈中。当主函数返回时,defer函数会从栈中弹出(先进后出的顺序)。
这里,将会先调用c( ),然后b ( ),最后是a( )。
注意:一个defer调用的时机是在函数返回时,而非在所在的块退出时。
再看一下上面例子用defer的实现:
func CopyFile(srcName, dstName string) error {
src, err := os.Open(srcName)
if err != nil {
return err
}
defer src.Close() //①
stat, err := src.Stat()
if err != nil {
return err
}
if stat.IsDir() {
return fmt.Errorf("file %q is a directory", srcName)
}
dst, err := os.Create(dstName)
if err != nil {
return err
}
defer dst.Close() //②
_, err = io.Copy(dst, src)
if err != nil {
return err
}
return dst.Sync()
}
① 延迟调用 src.Close()
② 延迟调用 dst.Close()
在这个版本的实现中,我们通过defer关键词的使用,移除了重复的close调用。这使得函数更轻量并且更易读。我们不必在每一个代码路径的末尾都关闭src和dst,这样就不容易出错了。
总结
defer语句经常会跟成对出现的操作函数一起使用,就像open/close,connect/disconnect,以及lock/unlock函数以确保在所有的场景下资源都能够得到释放。
总之,defer可以避免死板的代码以及减少忘记释放资源的风险,例如释放资源,断开链接,mutex解锁等等。
附一个有趣的用法
如果我们必须实现一个pre和post操作,比如不返回任何值的mutex lock/unlock,我们也可以这样实现:
func (s *Store) Set(key string, value int) {
defer s.lockUnlock()() //①
s.data[key] = value
}
func (s *Store) lockUnlock() func() {
s.mutex.Lock()
return func() {
s.mutex.Unlock()
}
}
① 该语句会立刻执行s.lockUnlock(),但会延迟执行s.lockUnlock()()
延迟执行的函数调用是s.lockUnlock()(),而非s.lockUnlock()。
因此,s.lockUnlock()部分会立即执行(s.mutex.Lock),但是返回的闭包会被延迟执行(s.mutex.Unlock())。
它添加了一些语法糖,用一行代码来处理函数中的前/后操作,这有时非常方便。