延宕执行,妙用无穷,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中defer关 ...

打印 上一主题 下一主题

主题 960|帖子 960|积分 2880

先行定义,延后执行。不得不佩服Go lang设计者天才的设计,事实上,defer关键字就相当于Python中的try{ ...}except{ ...}finally{...}结构设计中的finally语法块,函数结束时强制执行的代码逻辑,但是defer在语法结构上更加优雅,在函数退出前统一执行,可以随时增加defer语句,多用于系统资源的释放以及相关善后工作。当然了,这种流程结构是必须的,形式上可以不同,但底层原理是类似的,Golang 选择了更简约的defer,避免多级嵌套的try except finally 结构。
使用场景

操作系统资源在业务上避免不了的,比方说单例对象的使用权、文件读写、数据库读写、锁的获取和释放等等,这些资源需要在使用完之后释放掉或者销毁,如果忘记释放、资源会常驻内存,长此以往就会造成内存泄漏的问题。但是人非圣贤,孰能无过?因此研发者在撰写业务的时候有几率忘记关闭这些资源。
Golang中defer关键字的优势在于,在打开资源语句的下一行,就可以直接用defer语句来注册函数结束后执行关闭资源的操作。说白了就是给程序逻辑“上闹钟”,定义好逻辑结束时需要关闭什么资源,如此,就降低了忘记关闭资源的概率:
  1. package main  
  2.   
  3. import (  
  4.         "fmt"  
  5.         "github.com/jinzhu/gorm"  
  6.         _ "github.com/jinzhu/gorm/dialects/mysql"  
  7. )  
  8.   
  9. func main() {  
  10.         db, err := gorm.Open("mysql", "root:root@(localhost)/mytest?charset=utf8mb4&parseTime=True&loc=Local")  
  11.   
  12.         if err != nil {  
  13.                 fmt.Println(err)  
  14.                        fmt.Println("连接数据库出错")  
  15.                 return  
  16.         }  
  17.   
  18.         defer db.Close()  
  19.         fmt.Println("链接Mysql成功")  
  20.   
  21. }
复制代码
这里通过gorm获取数据库指针变量后,在业务开始之前就使用defer定义好数据库链接的关闭,在main函数执行完毕之前,执行db.Close()方法,所以打印语句是在defer之前执行的。
所以需要注意的是,defer最好在业务前面定义,如果在业务后面定义:
  1. fmt.Println("链接Mysql成功")  
  2. defer db.Close()
复制代码
这样写就是画蛇添足了,因为本来就是结束前执行,这里再加个defer关键字的意义就不大了,反而会在编译的时候增加程序的判断逻辑,得不偿失。
defer执行顺序问题

Golang并不会限制defer关键字的数量,一个函数中允许多个“延迟任务”:
  1. package main  
  2.   
  3. import "fmt"  
  4.   
  5. func main() {  
  6.         defer func1()  
  7.         defer func2()  
  8.         defer func3()  
  9. }  
  10.   
  11. func func1() {  
  12.         fmt.Println("任务1")  
  13. }  
  14.   
  15. func func2() {  
  16.         fmt.Println("任务2")  
  17. }  
  18.   
  19. func func3() {  
  20.         fmt.Println("任务3")  
  21. }
复制代码
程序返回:
  1. 任务3  
  2. 任务2  
  3. 任务1
复制代码
我们可以看到,多个defer的执行顺序其实是“反”着的,先定义的后执行,后定义的先执行,为什么?因为defer的执行逻辑其实是一种“压栈”行为:
  1. package main  
  2.   
  3. import (  
  4.         "fmt"  
  5.         "sync"  
  6. )  
  7.   
  8. // Item the type of the stack  
  9. type Item interface{}  
  10.   
  11. // ItemStack the stack of Items  
  12. type ItemStack struct {  
  13.         items []Item  
  14.         lock  sync.RWMutex  
  15. }  
  16.   
  17. // New creates a new ItemStack  
  18. func NewStack() *ItemStack {  
  19.         s := &ItemStack{}  
  20.         s.items = []Item{}  
  21.         return s  
  22. }  
  23.   
  24. // Pirnt prints all the elements  
  25. func (s *ItemStack) Print() {  
  26.         fmt.Println(s.items)  
  27. }  
  28.   
  29. // Push adds an Item to the top of the stack  
  30. func (s *ItemStack) Push(t Item) {  
  31.         s.lock.Lock()  
  32.         s.lock.Unlock()  
  33.         s.items = append(s.items, t)  
  34. }  
  35.   
  36. // Pop removes an Item from the top of the stack  
  37. func (s *ItemStack) Pop() Item {  
  38.         s.lock.Lock()  
  39.         defer s.lock.Unlock()  
  40.         if len(s.items) == 0 {  
  41.                 return nil  
  42.         }  
  43.         item := s.items[len(s.items)-1]  
  44.         s.items = s.items[0 : len(s.items)-1]  
  45.         return item  
  46. }
复制代码
这里我们使用切片和结构体实现了栈的数据结构,当元素入栈的时候,会进入栈底,后进的会把先进的压住,出栈则是后进的先出:
  1. func main() {  
  2.   
  3.         var stack *ItemStack  
  4.         stack = NewStack()  
  5.         stack.Push("任务1")  
  6.         stack.Push("任务2")  
  7.         stack.Push("任务3")  
  8.         fmt.Println(stack.Pop())  
  9.         fmt.Println(stack.Pop())  
  10.         fmt.Println(stack.Pop())  
  11.   
  12. }
复制代码
程序返回:
  1. 任务3  
  2. 任务2  
  3. 任务1
复制代码
所以,在defer执行顺序中,业务上需要先执行的一定要后定义,而业务上后执行的一定要先定义。
除此以外,就是与其他执行关键字的执行顺序问题,比方说return关键字:
  1. package main  
  2.   
  3. import "fmt"  
  4.   
  5. func main() {  
  6.         test()  
  7. }  
  8.   
  9. func test() string {  
  10.         defer fmt.Println("延时任务执行")  
  11.         return testRet()  
  12. }  
  13.   
  14. func testRet() string {  
  15.         fmt.Println("返回值函数执行")  
  16.         return ""  
  17. }
复制代码
程序返回:
  1. 返回值函数执行  
  2. 延时任务执行
复制代码
一般情况下,我们会认为return就是结束逻辑,所以return逻辑应该会最后执行,但实际上defer会在retrun后面执行,所以defer中的逻辑如果依赖return中的执行结果,那么就绝对不能使用defer关键字。
业务与特性结合

我们知道,有些内置关键字不仅仅具备表层含义,如果了解其特性,是可以参与业务逻辑的,比如说Python中的try{ ...}except{ ...}finally{...}结构,表面上是捕获异常,输出异常,其实可以利用其特性搭配唯一索引,就可以直接完成排重业务,从而减少一次磁盘的IO操作。
defer也如此,假设我们要在同一个函数中打开不同的文件进行操作:
  1. package main  
  2.   
  3. import (  
  4.         "os"  
  5. )  
  6.   
  7. func mergeFile() error {  
  8.   
  9.         f1, _ := os.Open("file1.txt")  
  10.         if f1 != nil {  
  11.   
  12.                 //操作文件  
  13.                 f1.Close()  
  14.         }  
  15.   
  16.         f2, _ := os.Open("file2.txt")  
  17.         if f2 != nil {  
  18.   
  19.                 //操作文件  
  20.                 f2.Close()  
  21.         }  
  22.   
  23.         return nil  
  24. }  
  25.   
  26. func main(){  
  27. mergeFile()  
  28. }
复制代码
所以理论上,需要两个文件句柄对象,分别打开不同的文件,然后同步执行。
但让defer关键字参与进来:
  1. package main  
  2.   
  3. import (  
  4.         "fmt"  
  5.         "io"  
  6.         "os"  
  7. )  
  8.   
  9. func mergeFile() error {  
  10.   
  11.         f, _ := os.Open("file1.txt")  
  12.         if f != nil {  
  13.                 defer func(f io.Closer) {  
  14.                         if err := f.Close(); err != nil {  
  15.                                 fmt.Printf("文件1关闭 err %v\n", err)  
  16.                         }  
  17.                 }(f)  
  18.         }  
  19.   
  20.         f, _ = os.Open("file2.txt")  
  21.         if f != nil {  
  22.                 defer func(f io.Closer) {  
  23.                         if err := f.Close(); err != nil {  
  24.                                 fmt.Printf("文件2关闭 err err %v\n", err)  
  25.                         }  
  26.                 }(f)  
  27.         }  
  28.   
  29.         return nil  
  30. }  
  31.   
  32. func main() {  
  33.   
  34.         mergeFile()  
  35. }
复制代码
这里就用到了defer的特性,defer函数定义的时候,句柄参数就已经复制进去了,随后,真正执行close()函数的时候就刚好关闭的是对应的文件了,如此,同一个句柄对不同文件进行了复用,我们就节省了一次内存空间的分配。
defer一定会执行吗

我们知道Python中的try{ ...}except{ ...}finally{...}结构,finally仅仅是理论上会执行,一旦遇到特殊情况:
  1. from peewee import MySQLDatabase  
  2.   
  3. class Db:  
  4.   
  5.     def __init__(self):  
  6.   
  7.         self.db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)  
  8.   
  9.     def __enter__(self):  
  10.         print("connect")  
  11.         self.db.connect()  
  12.         exit(-1)  
  13.   
  14.     def __exit__(self,*args):  
  15.         print("close")  
  16.         self.db.close()  
  17.   
  18. with Db() as db:  
  19.     print("db is opening")
复制代码
程序返回:
  1. connect
复制代码
并未执行print("db is opening")逻辑,是因为在__enter__方法中就已经结束了(exit(-1))
而defer同理:
  1. package main  
  2.   
  3. import (  
  4.         "fmt"  
  5.         "os"  
  6. )  
  7.   
  8. func main() {  
  9.         defer func() {  
  10.                 fmt.Printf("延后执行")  
  11.         }()  
  12.         os.Exit(1)  
  13. }
复制代码
这里和Python一样,同样调用os包中的Exit函数,程序返回:
  1. exit status 1
复制代码
延迟方法并未执行,所以defer并非一定会执行。
结语

defer关键字是极其天才的设计,业务简单的情况下不会有什么问题。但也需要深入理解defer的特性以及和其他内置关键字的关系,才能发挥它最大的威力,著名语言C#最新版本支持了 using无括号的形式,默认当前块结束时释放资源,这也算是对defer关键字的一种致敬罢。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

雁过留声

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表