Golang全面深入系列之 defer

90design · · 1357 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

 

前言

    大家都知道go语言的defer功能很强大,对于资源管理非常方便,但是如果没用好,也会有陷阱哦。Go 语言中延迟函数 defer 充当着 try...catch 的重任,使用起来也非常简便,那么defer、return、返回值、panic 之间的执行顺序是怎么样的呢,下面我们就一点一点来揭开它的神秘面纱!话不多说了,来一起看看介绍吧。

 

Defer介绍

defer语句用于函数在返回之前执行函数调用。这个定义可能看起来很复杂,但通过一个例子很容易理解。

package main

import (
	"fmt"
)

func finished() {
	fmt.Println("finished")
}

func largest() {
	defer finished()
	fmt.Println("largest()执行")
}

func main() {
	largest()
}

也就是在func结束(return)之前执行的动作。无论定义在普通语句的前后。

defer同样支持方法的调用。

package main

import (
	"fmt"
)


type person struct {
	firstName string
	lastName string
}

func (p person) fullName() {
	fmt.Printf("%s %s",p.firstName,p.lastName)
}

func main() {
	p := person {
		firstName: "John",
		lastName: "Smith",
	}
	defer p.fullName()
	fmt.Printf("Welcome ")
}

 

PS: defer声明时会先计算确定参数的值,defer推迟执行的仅是其函数体


import (  
    "fmt"
)

func printA(a int) {  
    fmt.Println("value of a in deferred function", a)
}
func main() {  
    a := 5
    defer printA(a)
    a = 10
    fmt.Println("value of a before deferred function call", a)

}

 当defer被声明时, 其参数a值就会被实时解析,并不执行函数体内内容, 那么后续的修改并不会影响当前参数值。

 

验证结果:

package main

import (
	"fmt"
	"time"
)

func main() {
	defer P(time.Now())
	time.Sleep(5*time.Second)
	fmt.Println("main ", time.Now())
}

func P(t time.Time) {
	fmt.Println("defer", t)
	fmt.Println("P ", time.Now())
}

执行结果:

main  2018-03-16 14:43:25.10348 +0800 CST m=+5.003171200
defer 2018-03-16 14:43:20.1033124 +0800 CST m=+0.003003600
P  2018-03-16 14:43:25.142031 +0800 CST m=+5.041722200

 

defer后进先出

简单的理解为先声明的defer后执行(倒序执行defer语句)。

package main

import (  
    "fmt"
)

func main() {  
    name := "Naveen"
    fmt.Printf("Orignal String: %s\n", string(name))

    for _, v := range []rune(name) {
        defer fmt.Printf("%c", v)
    }
}

输出为: neevaN

 

defer / return / 返回值 / panic之间的执行顺序

现在我们分两种情况分析它们的执行顺序:

1. return前将返回值赋值

2. 检查是否有defer并执行

3. 最后return 携带返回值退出函数

 

第一种: 匿名返回值

package main

import (
	"fmt"
)

func main() {
	fmt.Println("a return:", a()) // 打印结果为 a return: 0
}

func a() int {
	var i int
	defer func() {
		i++
		fmt.Println("a defer2:", i) // 打印结果为 a defer2: 2
	}()
	defer func() {
		i++
		fmt.Println("a defer1:", i) // 打印结果为 a defer1: 1
	}()
	return i
}

输出结果:

a defer1: 1
a defer2: 2
a return: 0

解释: 匿名返回值是在return之前被声明( 鉴于类型原因,类型零值为0 ), defer无法访问匿名的返回值,因此返回值是0, 而defer还是操作之前定义好的变量i。

试想:如果此处返回值为指针类型,那么输出结果会不会有变化?

 

第二种:命名返回值

package main

import (
	"fmt"
)

func main() {
	fmt.Println("a return:", a()) // 打印结果为 b return: 2
}

func a() (i int) {
	defer func() {
		i++
		fmt.Println("a defer2:", i) // 打印结果为 b defer2: 2
	}()
	defer func() {
		i++
		fmt.Println("a defer1:", i) // 打印结果为 b defer1: 1
	}()
	return i // 或者直接 return 效果相同
}

输出结果:

a defer1: 1
a defer2: 2
a return: 2

解释: 命名返回值是 在函数声明的同时被声明。因此defer可以访问命名返回值。return返回后的值其实是defer修改后的值。

 

defer作用域

defer在什么环境下就不会执行?下面列举几个例子:

1. 当任意一条(主)协程发生 panic 时,会执行当前协程中 panic 之前已声明的 defer;


func main() {
	fmt.Println("...")
	panic(e) // defer 不会执行
	defer fmt.Println("defer")
}

 

2. 主动调用 os.Exit(int) 退出进程时,defer 将不再被执行。

func main() {
	fmt.Println("...")
	os.Exit(1) // defer 不会执行
	defer fmt.Println("defer")
}
func main() {
	fmt.Println("...")
	defer fmt.Println("defer")
	os.Exit(1) // defer 不会执行
}

 

3.在发生 panic 的(主)协程中,如果没有一个 defer 调用 recover()进行恢复,则会在执行完之前已声明的 defer 后,引发整个进程崩溃;

func main() {
	fmt.Println("...")
	defer fmt.Println("defer1")
	defer fmt.Println("defer2")
	defer fmt.Println("defer3")

	panic("error") // defer 会执行
}

 

4. defer只对当前(主)协程有效

package main

import (
	"fmt"
)

func main() {
	fmt.Println("...")
	go func() { panic("err") }() // defer 执行
	defer fmt.Println("defer1")
	defer fmt.Println("defer2")
	defer fmt.Println("defer3")
}

 

defer的实际用途

无论什么业务或程序, 延迟都用在执行函数调用的地方。

举例:

后期更新

 

 

 


有疑问加站长微信联系(非本文作者)

本文来自:开源中国博客

感谢作者:90design

查看原文:Golang全面深入系列之 defer

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1357 次点击  ∙  1 赞  
加入收藏 微博
1 回复  |  直到 2018-03-19 16:31:09
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传