常记心中
理工科的学习一定要以基础知识为根本,基础知识又以概念的定义为基石。
基础知识里面的概念作为基石,都有其存在的必要性,所以当我们清楚了概念的同时,也要弄清楚其为什么存在。而为什么存在就是要搞清楚概念之间的联系。当探索的越来越多,我们可以说,在这个特定领域,最起码,我们站住脚了。继续前进吧~
defer的定义
A defer statement defers the execution of a function until the surrounding function returns.
一句话总结:defer就是用来延迟执行函数的技术手段。
虽然只有短短一句话,但是我们也可以获得很多。
来来来,我们先来总结一下,(更多的内容将在下文展开论述)。
1、defer后面跟函数:defers the execution of a function。
2、defer的作用就是激活后接的函数:the execution.
3、运行时机:until the surrounding function returns.
A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.
为什么要有defer
defer的本质就是延迟自动执行函数。
它的出现就是为了更加便利的进行资源管理,减轻程序员的心智负担,提高代码的可读性、可维护性和可扩展性。
假如go没有defer机制,为了实现一个受保护的文件操作。我们得这么写
func writeToFile(fname string, data []byte, mu *sync.Mutex) error {
mu.Lock()
f, err := os.OpenFile(fname, os.O_RDWR, 0666)
if err != nil {
mu.Unlock()
return err
}
_, err = f.Seek(0, 2)
if err != nil {
f.Close()
mu.Unlock()
return err
}
_, err = f.Write(data)
if err != nil {
f.Close()
mu.Unlock()
return err
}
err = f.Sync()
if err != nil {
f.Close()
mu.Unlock()
return err
}
err = f.Close()
if err != nil {
mu.Unlock()
return err
}
mu.Unlock()
return nil
}
就像老奶奶的裹脚布——又臭又长。程序员每次操作,都得处理资源的释放:锁的释放、文件句柄的关闭。仅仅两个资源的释放操作,就得『如临深渊,如履薄冰』。增加资源的参与数,那简直不堪设想。
幸好,我们的Go有defer机制,我们可以改写如下
func writeToFile(fname string, data []byte, mu *sync.Mutex) error {
mu.Lock()
defer mu.Unlock()
f, err := os.OpenFile(fname, os.O_RDWR, 0666)
if err != nil {
return err
}
defer f.Close()
_, err = f.Seek(0, 2)
if err != nil {
return err
}
_, err = f.Write(data)
if err != nil {
return err
}
return f.Sync()
}
我们所需要做的就是申请资源之后,马上defer+释放资源的逻辑。当writeToFile运行结束时,会自动释放锁和关闭文件句柄。
让我们编写需要释放资源的代码时非常的便利!
defer的特性
上面对defer的描述,只是非常粗糙的讲解。下面讲述为了释放资源,延迟执行,defer的具体规定细节。
比如
defer后接函数,那么函数的参数什么时候确定?(要知道同样的变量,在函数退出时与刚进入函数时,内容可能是不一样的)可以后接方法吗?
defer后的函数真正执行时机是:声明defer表达式的函数执行结束时。那么这个所谓的函数执行结束时,是返回后,还是返回前?还是其他什么时间点?
如果一个函数之中,声明了多个defer表达式,虽然都是函数退出时调用,但是它们执行的顺序如何?随机的?(像select channel一样)?先进先出?(越先被defer的函数越早调用)?先进后出?
来吧,让我们回答上面的问题。
不过在此之前,学习两个英文单词。
execution:中文是执行的意思。比如函数的执行,就是运行函数的意思。
evaluate:中文是评价、估计的意思。在编程里面,比如函数参数,当我们说evaluate function parameters,就是确定函数参数值的意思。嗯,evaluate可以翻译成确定。
下面进入正题
1、defered function evaluates its parameter when is occur
被defer的函数会立马确定后接函数参数
func print(num int) {
fmt.Println(num)
}
func foo() {
var i = 666
defer print(i)
i = 888
return
}
func main() {
foo()
}
会打印666。这就叫做当出现defer时,后接函数的参数立马被确定,即i=666。虽然defer在后面才会执行,但是参数的确定是在一开始就确定了。
2、defered function will execute before real return
被defer的函数会在调用它的函数返回前执行
这句话不好理解。重点在于什么叫做real return。
休息一下,下面的例子需要仔细看
func sara() (result int) {
result = 1
defer func() {
result = 666
}()
return result
}
func lisa() int {
var result = 1
defer func() {
result = 666
}()
return result
}
func main() {
r := sara()
fmt.Printf("sara finially result, %d\n", r) // 666
r = lisa()
fmt.Printf("lisa finially result, %d\n", r) // 1
}
可以观察到,sara和lisa基本逻辑是一样的,唯一的不同就是sara是具名返回值函数,而lisa是匿名返回值函数。这就是大大的不同了。
好好理解一下上面的例子,下面给出go函数的返回值模型
其实go的return语句是分成两部分的。比如对于匿名返回值函数lisa来说
return result
相当于
1、var ret = result // ret 是真返回值
2、return ret
对于具名返回值函数sara来说
return result
相当于
1、var result = result
2、return result
注意两者的区别。
重点来啦,defer的运行时机就是步骤1和步骤2之间!
所以对于lisa来说,我要返回的是ret,就算你在defer中修改了result,也不会影响到最终返回值哦。但是sara就不一样了,她返回的就是result,在defer中修改了result,最终的结果就不一样啦。
咳咳,具名返回值和defer的联合使用,要注意哦😑。别写这样的代码。
3、多个defer的执行的顺序是:后进先出,类似栈
func print(num int) {
fmt.Println(num)
}
func bob() {
defer print(1)
defer print(2)
defer print(3)
return
}
func main() {
bob()
}
会打印
3
2
1
如何记住defer的特性
defer出现就是为了资源的管理。
比如如果有以下这个过程:首先获取资源A,然后获取资源B,最后获取资源C才能执行某个操作,那么defer的顺序,肯定是先放回C资源,然后B资源,最后是A资源。
这个过程让我想起来的举证的乘法和逆运算——逆矩阵。
比如AB,表示矩阵A乘以矩阵B,那么它俩的逆运算呢?
当时为了记住这个特性,我是记住了下面的这个类比:
先穿袜子,然后穿鞋;如果这一过程反过来,那就是先脱鞋,再脱袜子。
nice。
如今看来,不论是穿衣服、矩阵乘法、资源的管理,都是一种行为、一种运动、一种变化。他们是分先后的,所以他们的“反运动”要与正向运动相反。
真实世界中的使用案例
这个还真没遇到特别精彩的使用。都是中规中矩的释放资源,在上面已经有了多个例子。
姑且等待补充吧~
参考
-
go官方文档和教程 -
<100 Go mistakes> -
<The Golang Programming Language> -
白明《Go语言精进之路》