> 文章列表 > 深入理解Go语言中的函数【延迟调用defer】12

深入理解Go语言中的函数【延迟调用defer】12

深入理解Go语言中的函数【延迟调用defer】12

文章目录

    • 延迟调用defer
      • defer特性:
      • defer用途:
        • defer 碰上闭包
        • defer f.Close
      • defer陷阱
        • defer 与 closure
        • defer 与 return
        • defer nil 函数
        • 错误的位置使用 defer
        • 解决方案
        • 不检查错误

延迟调用defer

defer特性:

    1. 关键字 defer 用于注册延迟调用。2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。3. 多个defer语句,按先进后出的方式执行。4. defer语句中的变量,在defer声明时就决定了。

defer用途:

    1. 关闭文件句柄2. 锁资源释放3. 数据库连接释放
  • defer用于注册一个延迟调用(在函数返回之前调用)。
  • defer典型的应用场景是释放资源,比如关闭文件句柄,释放数据库连接等。
  • 如果同一个函数里有多个defer,则后注册的先执行。
  • defer后可以跟一个func,func内部如果发生panic,会把panic暂时搁置,当把其他defer执行完之后再来执行这个。
  • defer后不是跟func,而直接跟一条执行语句,则相关变量在注册defer时被拷贝或计算。
func basic() {fmt.Println("A")defer fmt.Println(1)//如果同一个函数里有多个defer,则后注册的先执行defer fmt.Println(2)fmt.Println("C")
}
// A C 2 1
func defer_exe_time() (i int) {i = 9defer func() { //defer后可以跟一个funcfmt.Printf("first i=%d\\n", i) //打印5,而非9。充分理解“defer在函数返回前执行”的含义,不是在“return语句前执行defer”}()defer func(i int) {fmt.Printf("second i=%d\\n", i) //打印9}(i)defer fmt.Printf("third i=%d\\n", i) //defer后不是跟func,而直接跟一条执行语句,则相关变量在注册defer时被拷贝或计算return 5
}
third i=9
second i=9
first i=5
func defer_panic() {defer fmt.Println("111")n := 0defer func() {fmt.Println(2 / n)fmt.Println("222")}()fmt.Println("333")
}
333
111
panic

defer 碰上闭包

package mainimport "fmt"func main() {var whatever [5]struct{}for i := range whatever {defer func() { fmt.Println(i) }()}
} 

输出结果:

    44444

其实go说的很清楚,我们一起来看看go spec如何说的

Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.

也就是说函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4.

defer f.Close

这个大家用的都很频繁,但是go语言编程举了一个可能一不小心会犯错的例子.

package mainimport "fmt"type Test struct {name string
}func (t *Test) Close() {fmt.Println(t.name, " closed")
}
func main() {ts := []Test{{"a"}, {"b"}, {"c"}}for _, t := range ts {defer t.Close()}
} 

输出结果:

    c  closedc  closedc  closed

这个输出并不会像我们预计的输出c b a,而是输出c c c

可是按照前面的go spec中的说明,应该输出c b a才对啊.

那我们换一种方式来调用一下.

package mainimport "fmt"type Test struct {name string
}func (t *Test) Close() {fmt.Println(t.name, " closed")
}
func Close(t Test) {t.Close()
}
func main() {ts := []Test{{"a"}, {"b"}, {"c"}}for _, t := range ts {defer Close(t)}
} 

输出结果:

    c  closedb  closeda  closed

这个时候输出的就是c b a

当然,如果你不想多写一个函数,也很简单,可以像下面这样,同样会输出c b a

看似多此一举的声明

package mainimport "fmt"type Test struct {name string
}func (t *Test) Close() {fmt.Println(t.name, " closed")
}
func main() {ts := []Test{{"a"}, {"b"}, {"c"}}for _, t := range ts {t2 := tdefer t2.Close()}
} 

输出结果:

    c  closedb  closeda  closed

通过以上例子,结合

Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.

这句话。可以得出下面的结论:

defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份。但是并没有说struct这里的this指针如何处理,通过这个例子可以看出go语言并没有把这个明确写出来的this指针当作参数来看待。

多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。

package mainfunc test(x int) {defer println("a")defer println("b")defer func() {println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。}()defer println("c")
}func main() {test(0)
} 

输出结果:

    cbapanic: runtime error: integer divide by zero

*延迟调用参数在注册时求值或复制,可用指针或闭包 “延迟” 读取。

package mainfunc test() {x, y := 10, 20defer func(i int) {println("defer:", i, y) // y 闭包引用}(x) // x 被复制x += 10y += 100println("x =", x, "y =", y)
}func main() {test()
}  

输出结果:

    x = 20 y = 120defer: 10 120

*滥用 defer 可能会导致性能问题,尤其是在一个 “大循环” 里。

package mainimport ("fmt""sync""time"
)var lock sync.Mutexfunc test() {lock.Lock()lock.Unlock()
}func testdefer() {lock.Lock()defer lock.Unlock()
}func main() {func() {t1 := time.Now()for i := 0; i < 10000; i++ {test()}elapsed := time.Since(t1)fmt.Println("test elapsed: ", elapsed)}()func() {t1 := time.Now()for i := 0; i < 10000; i++ {testdefer()}elapsed := time.Since(t1)fmt.Println("testdefer elapsed: ", elapsed)}()}

输出结果:

    test elapsed:  223.162µstestdefer elapsed:  781.304µs

defer陷阱

defer 与 closure

package mainimport ("errors""fmt"
)func foo(a, b int) (i int, err error) {defer fmt.Printf("first defer err %v\\n", err)defer func(err error) { fmt.Printf("second defer err %v\\n", err) }(err)defer func() { fmt.Printf("third defer err %v\\n", err) }()if b == 0 {err = errors.New("divided by zero!")return}i = a / breturn
}func main() {foo(2, 0)
}  

输出结果:

    third defer err divided by zero!second defer err <nil>first defer err <nil>

解释:如果 defer 后面跟的不是一个 closure 最后执行的时候我们得到的并不是最新的值。

defer 与 return

package mainimport "fmt"func foo() (i int) {i = 0defer func() {fmt.Println(i)}()return 2
}func main() {foo()
}

输出结果:

    2

解释:在有具名返回值的函数中(这里具名返回值为 i),执行 return 2 的时候实际上已经将 i 的值重新赋值为 2。所以defer closure 输出结果为 2 而不是 1。

defer nil 函数

package mainimport ("fmt"
)func test() {var run func() = nildefer run()fmt.Println("runs")
}func main() {defer func() {if err := recover(); err != nil {fmt.Println(err)}}()test()
} 

输出结果:

runs
runtime error: invalid memory address or nil pointer dereference

解释:名为 test 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。然而值得注意的是,run() 的声明是没有问题,因为在test函数运行完成后它才会被调用。

在错误的位置使用 defer

当 http.Get 失败时会抛出异常。

package mainimport "net/http"func do() error {res, err := http.Get("http://www.google.com")defer res.Body.Close()if err != nil {return err}// ..code...return nil
}func main() {do()
} 

输出结果:

    panic: runtime error: invalid memory address or nil pointer dereference

因为在这里我们并没有检查我们的请求是否成功执行,当它失败的时候,我们访问了 Body 中的空变量 res ,因此会抛出异常

解决方案

总是在一次成功的资源分配下面使用 defer ,对于这种情况来说意味着:当且仅当 http.Get 成功执行时才使用 defer

package mainimport "net/http"func do() error {res, err := http.Get("http://xxxxxxxxxx")if res != nil {defer res.Body.Close()}if err != nil {return err}// ..code...return nil
}func main() {do()
} 

在上述的代码中,当有错误的时候,err 会被返回,否则当整个函数返回的时候,会关闭 res.Body 。

解释:在这里,你同样需要检查 res 的值是否为 nil ,这是 http.Get 中的一个警告。通常情况下,出错的时候,返回的内容应为空并且错误会被返回,可当你获得的是一个重定向 error 时, res 的值并不会为 nil ,但其又会将错误返回。上面的代码保证了无论如何 Body 都会被关闭,如果你没有打算使用其中的数据,那么你还需要丢弃已经接收的数据。

不检查错误

在这里,f.Close() 可能会返回一个错误,可这个错误会被我们忽略掉

package mainimport "os"func do() error {f, err := os.Open("book.txt")if err != nil {return err}if f != nil {defer f.Close()}// ..code...return nil
}func main() {do()
}  

改进一下

package mainimport "os"func do() error {f, err := os.Open("book.txt")if err != nil {return err}if f != nil {defer func() {if err := f.Close(); err != nil {// log etc}}()}// ..code...return nil
}func main() {do()
} 

再改进一下

通过命名的返回变量来返回 defer 内的错误。

package mainimport "os"func do() (err error) {f, err := os.Open("book.txt")if err != nil {return err}if f != nil {defer func() {if ferr := f.Close(); ferr != nil {err = ferr}}()}// ..code...return nil
}func main() {do()
} 

释放相同的资源

如果你尝试使用相同的变量释放不同的资源,那么这个操作可能无法正常执行。

package mainimport ("fmt""os"
)func do() error {f, err := os.Open("book.txt")if err != nil {return err}if f != nil {defer func() {if err := f.Close(); err != nil {fmt.Printf("defer close book.txt err %v\\n", err)}}()}// ..code...f, err = os.Open("another-book.txt")if err != nil {return err}if f != nil {defer func() {if err := f.Close(); err != nil {fmt.Printf("defer close another-book.txt err %v\\n", err)}}()}return nil
}func main() {do()
} 

输出结果:
defer close book.txt err close ./another-book.txt: file already closed

当延迟函数执行时,只有最后一个变量会被用到,因此,f 变量 会成为最后那个资源 (another-book.txt)。而且两个 defer 都会将这个资源作为最后的资源来关闭

解决方案:

package mainimport ("fmt""io""os"
)func do() error {f, err := os.Open("book.txt")if err != nil {return err}if f != nil {defer func(f io.Closer) {if err := f.Close(); err != nil {fmt.Printf("defer close book.txt err %v\\n", err)}}(f)}// ..code...f, err = os.Open("another-book.txt")if err != nil {return err}if f != nil {defer func(f io.Closer) {if err := f.Close(); err != nil {fmt.Printf("defer close another-book.txt err %v\\n", err)}}(f)}return nil
}func main() {do()
}