延迟调用函数已经在前面介绍过了。 限于当时对Go的了解程度,很多延迟调用函数相关的细节和用例并没有在之前的文章中提及。 这些细节和用例将在本文中列出。
在Go中,自定义函数的调用的返回结果都可以被舍弃。
但是,大多数内置函数(除了copy
和recover
)的调用的返回结果都不可以舍弃(至少对于Go 1.23来说是如此)。
另一方面,我们已经了解到延迟函数调用的所有返回结果必须都舍弃掉。
所以,很多内置函数是不能被延迟调用的。
append
函数有时可能会需要被延迟调用。
对于这种情形,我们可以延迟调用一个调用了append
函数的匿名函数来满足这个需求。
package main
import "fmt"
func main() {
s := []string{"a", "b", "c", "d"}
defer fmt.Println(s) // [a x y d]
// defer append(s[:1], "x", "y") // 编译错误
defer func() {
_ = append(s[:1], "x", "y")
}()
}
false
。
package main
import "fmt"
func main() {
var f = func () {
fmt.Println(false)
}
defer f()
f = func () {
fmt.Println(true)
}
}
一个被延迟调用的函数值可能是一个nil函数值。这种情形将导致一个恐慌。
对于这种情形,恐慌产生在此延迟调用被执行而不是被推入延迟调用队列的时候。
一个例子:
package main
import "fmt"
func main() {
defer fmt.Println("此行可以被执行到")
var f func() // f == nil
defer f() // 将产生一个恐慌
fmt.Println("此行可以被执行到")
f = func() {} // 此行不会阻止恐慌产生
}
1342
。
package main
type T int
func (t T) M(n int) T {
print(n)
return t
}
func main() {
var t T
// t.M(1)是方法调用M(2)的属主实参,因此它
// 将在M(2)调用被推入延迟调用队列时被估值。
defer t.M(1).M(2)
t.M(3).M(4)
}
import "os"
func withoutDefers(filepath string, head, body []byte) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
_, err = f.Seek(16, 0)
if err != nil {
f.Close()
return err
}
_, err = f.Write(head)
if err != nil {
f.Close()
return err
}
_, err = f.Write(body)
if err != nil {
f.Close()
return err
}
err = f.Sync()
f.Close()
return err
}
func withDefers(filepath string, head, body []byte) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
defer f.Close()
_, err = f.Seek(16, 0)
if err != nil {
return err
}
_, err = f.Write(head)
if err != nil {
return err
}
_, err = f.Write(body)
if err != nil {
return err
}
return f.Sync()
}
上面哪个函数看上去更简洁?显然,第二个使用了延迟调用的函数,虽然只是简洁了些许。
另外第二个函数将导致更少的bug,因为第一个函数中含有太多的f.Close()
调用,从而有较高的几率漏掉其中一个。
doSomething
函数产生一个恐慌,则函数f2
在退出时将导致互斥锁未解锁。
所以函数f1
更鲁棒。
var m sync.Mutex
func f1() {
m.Lock()
defer m.Unlock()
doSomething()
}
func f2() {
m.Lock()
doSomething()
m.Unlock()
}
延迟调用并非没有缺点。对于早于1.13版本的官方标准编译器来说,延迟调用将导致一些性能损失。 从Go官方工具链1.13版本开始,官方标准编译器对一些常见的延迟调用场景做了很大的优化。 因此,一般我们不必太在意延迟调用导致的性能损失。感谢Dan Scales实现了此优化。
一个较大的延迟调用队列可能会消耗很多内存。 另外,某些资源可能因为某些调用被延迟的太久而未能被及时释放。
比如,如果下面的例子中的函数需要处理大量的文件,则在此函数退出之前,将有大量的文件句柄得不到释放。func writeManyFiles(files []File) error {
for _, file := range files {
f, err := os.Open(file.path)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(file.content)
if err != nil {
return err
}
err = f.Sync()
if err != nil {
return err
}
}
return nil
}
对于这种情形,我们应该使用一个匿名函数将需要及时执行延迟的调用包裹起来。比如,上面的函数可以改进为如下:
func writeManyFiles(files []File) error {
for _, file := range files {
if err := func() error {
f, err := os.Open(file.path)
if err != nil {
return err
}
defer f.Close() // 将在此循环步内执行
_, err = f.WriteString(file.content)
if err != nil {
return err
}
return f.Sync()
}(); err != nil {
return err
}
}
return nil
}
Go101.org网站内容包括Go编程各种相关知识(比如Go基础、Go优化、Go细节、Go实战、Go测验、Go工具等)。后续将不断有新的内容加入。敬请收藏关注期待。
本丛书微信公众号(联系方式一)名称为"Go 101"。二维码在网站首页。此公众号将时不时地发表一些Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。
《Go语言101》系列丛书项目目前托管在Github上(联系方式二)。欢迎各位在此项目中通过提交bug和PR的方式来改进完善《Go语言101》丛书中的各篇文章。我们可以在项目目录下运行go run .
来浏览和确认各种改动。
本书的twitter帐号为@Golang_101(联系方式三)。玩推的Go友可以适当关注。
你或许对本书作者老貘开发的一些App感兴趣。
sync
标准库包sync/atomic
标准库包