go中的Defer

202 阅读3分钟

前言

Go中控制流程的机制是普通的,和其他语言的流程控制相似: if, for, switch, goto。它也有go语句,可以使得代码在一个单独的goroutine中运行。现在,我将讨论和其他语言不同的部分:defer, panic, 和recover。

defer操作会将函数放到了一个list之中。存在此list之中的函数会在所在函数return之后执行。defer通常用于处理各种不同的清理操作。

例如,我们来看一个函数,此函数会打开两个文件,并将一个文件的内容复制到另一个文件之中:

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.Create(dstName)创建文件失败的情况下,此函数会在没有关闭打开的源文件的情况下,直接返回。这个问题可以非常简单解决,在函数中第二个return的前面加一行代码src.Close(),用于关闭源文件。但是在代码更复杂的时候,这种问题可能不是那么容易就可以发现和解决的。通过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,我们可以在打开文件的时候,就思考关闭文件。并且可以保证在无论函数中多少个return的情况下,都可以在函数返回的时候,关闭文件。

参考原则

defer操作是非常直接和可预测的。有三个简单的原则:

1.defer的函数参数,会在defer调用的时候就确定。

例子下面的例子,由于变量i的初始化值为1,在defer的函数调用时,此值i仍然为1。

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

defer的函数可以使一个对象的方法,此时可能会让人难以理解。

defer的函数的对象是一个对象的时候(非指针),下面函数会打印DeLorean DMC-12

type Car struct {
  model string
}
func (c Car) PrintModel() {
  fmt.Println(c.model)
}
func main() {
  c := Car{model: "DeLorean DMC-12"}  
  defer c.PrintModel()  
  c.model = "Chevrolet Impala"
}

defer的函数的对象是一个指针,下面函数会打印Chevrolet Impala

type Car struct {
  model string
}
func (c *Car) PrintModel() {
  fmt.Println(c.model)
}
func main() {
  c := &Car{model: "DeLorean DMC-12"}  
  defer c.PrintModel()  
  c.model = "Chevrolet Impala"
}

defer的函数是一个方法的时候,背后发生了如下的变换,函数接受一个对象的时候,传递一个对象作为参数,是复制了一份。所以在使用对象的时候,打印的是传入对象的model值,而使用指针调用方法的时候,指针指向的对象model发生了修改,所以打印的是修改后的值。

func (receiver Type) f(input) result -> func(recevier Type, input) result

  1. 保存defer的函数list,是一个栈,顺序为 Last In First Out

下面函数调用的时候,打印值为3210

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}
  1. defer的函数可以读取并修改函数的返回值

下面函数的返回值是i,而defer的函数可以读取和修改变量i,此函数会在return之后执行,所以此函数的返回值实际为2。

func c() (i int) {
    defer func() { i++ }()
    return 1
}

参考文献

  1. blog.golang.org/defer-panic…
  2. blog.learngoprogramming.com/golang-defe…