Skip to content
返回

Go 真的需要 block defer 吗?

发现错误了吗?

我没有考虑过这个问题

起因是微信群里面有一个群友,问了我一嘴这个问题,为啥 Go 实现了函数级的 defer 却没有实现 block defer?我一开始用少就是多搪塞过去了,感觉 Go 就是这么设计的。后面吃晚饭的时候又仔细的考虑了一下,对啊,为啥不这么设计?于是就有了这篇文章。

现有的 defer 机制

其实这个完全可以去看我之前的文章,这里我就再简单的概述一下吧。Go 语言里的 defer 关键字,是个很有特色也很实用的功能A。它能让函数调用延迟执行,常用在清理资源、解锁或者处理错误这些场景。带 defer 的语句,会在所在的函数返回前执行 —— 不管函数是正常结束,还是出了错停下来,它都会跑。这种设计挺好的,能让释放资源的代码和获取资源的代码挨得近,读起来更清楚,用起来也更安全。

但是,Go 语言的 defer 目前仅限于函数级别,即所有 d 语句 defer 都在函数返回时执行,而不是在代码块结束时执行。这就引出了一个有趣的问题:Go 语言是否有必要引入 block defer(块级 defer)机制,使得 defer 可以作用于特定的代码块而非整个函数?

核心特性

Go 的 defer 有下面这么几个特性:

上面的几个特性让 defer 特别适合成双成对的操作,就和找对象一样。比如说打开了文件就得关闭,加了锁你就得解锁,连接了你就得断开,确保资源无论如何都能被正常的释放。

实现机制

go 的 defer 经历了很多个阶段:

为啥会有 block defer 这个概念?

block defer 指的是在特定的代码块结束的时候执行 defer 语句,而不是等到整个函数结束的时候执行,预期的行为大概是:

假设我们有下面的这段代码:

func example() {
    // 块A开始
    {
        defer fmt.Println("Block A defer")
        // 块A中的代码
    } // 块A结束,此时应执行"Block A defer"
    // 块B开始
    {
        defer fmt.Println("Block B defer")
        // 块B中的代码
    } // 块B结束,此时应执行"Block B defer"
}

在当前的 go 语言里面,上面代码块中的两个 defer 语句都会在 example 函数返回的时候执行,顺序就是:Block B defer >>> Block A defer,如果这个时候我们引入 block defer 机制,输出的顺序就变成了 Bloack A defer >>> Block B defer,也就是再每个块结束的时候立马就执行这个块的 defer 语句。

可能的使用场景

block defer 可能会在下面这些个场景使用:

真的需要吗?

支持引入 block defer 的主要观点就是可维护性和可读性。站在这两个观点出发就不难理解为啥我们很需要 block defer 了:

就比如下面的代码对比:

func example() {
    // 块A开始
    func() {
        f, err := os.Open("file.txt")
        if err != nil {
            // 处理错误
        }
        defer f.Close()
        // 块A中的代码
    }() // 块A结束,f.Close()在此处执行
    // 块B开始
    func() {
        conn, err := openDBConnection()
        if err != nil {
            // 处理错误
        }
        defer conn.Close()
        // 块B中的代码
    }() // 块B结束,conn.Close()在此处执行
}

使用 block defer 机制可以避免创建这些额外的匿名函数,使代码更加简洁:

func example() {
    // 块A开始
    {
        f, err := os.Open("file.txt")
        if err != nil {
            // 处理错误
        }
        defer f.Close() // 块A结束时执行
        // 块A中的代码
    } // 块A结束,f.Close()在此处执行
    // 块B开始
    {
        conn, err := openDBConnection()
        if err != nil {
            // 处理错误
        }
        defer conn.Close() // 块B结束时执行
        // 块B中的代码
    } // 块B结束,conn.Close()在此处执行
}

一致性与完整性

另一个支持引入 block defer 的观点是语言设计的一致性和完整性:

现有替代方案真的很有效吗?

有效,但不够优雅。

比如前面提到的需求,就可以用嵌套函数去解决:

func example() {
    // 块A开始
    func() {
        f, err := os.Open("file.txt")
        if err != nil {
            // 处理错误
        }
        defer f.Close()
        // 块A中的代码
    }() // 块A结束,f.Close()在此处执行
    // 块B开始
    func() {
        conn, err := openDBConnection()
        if err != nil {
            // 处理错误
        }
        defer conn.Close()
        // 块B中的代码
    }() // 块B结束,conn.Close()在此处执行
}

这种方式虽然增加了一些代码量,但实现了与 block defer 类似的效果,并且不需要修改语言本身。

普通defer与block defer的实践

嵌套函数的解决方案

在当前 Go 语言中,实现块级资源管理的主要方法是使用嵌套函数:

func main() {
    // 块A
    {
        // 资源获取
        f, err := os.Open("file.txt")
        if err != nil {
            // 处理错误
        }
        // 使用匿名函数立即执行
        func() {
            defer f.Close()
            // 块A中的代码
        }()
        // 块A结束,此时f已经被关闭
    }
    // 块B
    {
        // 资源获取
        conn, err := database.Connect()
        if err != nil {
            // 处理错误
        }
        // 使用匿名函数立即执行
        func() {
            defer conn.Close()
            // 块B中的代码
        }()
        // 块B结束,此时conn已经被关闭
    }
}

这种方法的原理是:

  1. 在需要块级资源管理的位置创建一个匿名函数。
  2. 在匿名函数内部使用defer注册资源释放操作。
  3. 立即执行该匿名函数,确保defer在匿名函数返回时执行,即块结束时执行。

这种方法的优点是:

缺点是:

其他方案

除了嵌套函数外,还有一些其他方法可以实现类似块级资源管理的效果:

  1. 手动管理资源:在块结束前显式调用清理函数,不需要依赖defer:
func main() {
    // 块A
    {
        f, err := os.Open("file.txt")
        if err != nil {
            // 处理错误
        }
        // 块A中的代码
        f.Close() // 手动关闭文件
    }
}

这种方法的缺点是需要手动管理资源释放,容易遗漏,特别是在有多个返回点的情况下。

  1. 使用defer与条件判断结合:在某些情况下,可以通过条件判断来模拟块级defer的效果:
func main() {
    // 块A
    {
        f, err := os.Open("file.txt")
        if err != nil {
            // 处理错误
        }
        defer func() {
            if f != nil {
                f.Close()
            }
        }()
        // 块A中的代码
        f = nil // 提前清理,避免在块结束时关闭
    }
}

这种方法的缺点是需要额外的条件判断和变量管理,增加了代码的复杂性。

  1. 使用第三方库:一些第三方库(如errd)提供了更高级的资源管理功能,可以简化块级资源管理的代码。

发现错误了吗?

上一篇文章
一个基于图像特征比较的图片相似度匹配工具
下一篇文章
Go Defer的魔法和陷阱