延宕执行,妙用无穷,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中defer关键字延迟调用机制使用EP17

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 先行定义,延后执行。不得不佩服Go lang设计者天才的设计,事实上,defer关键字就相当于Python中的try{ ...}except{ ...}finally{...}结构设计中的finally语法块,函数结束时强制执行的代码逻辑,但是defer在语法结构上更加优雅,在函数退出前统一执行,可以随时增加defer语句,多用于系统资源的释放以及相关善后工作。当然了,这种流程结构是必须的,形式上可以不同,但底层原理是类似的,Golang 选择了更简约的defer,避免多级嵌套的try except finally 结构。

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

使用场景

操作系统资源在业务上避免不了的,比方说单例对象的使用权、文件读写、数据库读写、锁的获取和释放等等,这些资源需要在使用完之后释放掉或者销毁,如果忘记释放、资源会常驻内存,长此以往就会造成内存泄漏的问题。但是人非圣贤,孰能无过?因此研发者在撰写业务的时候有几率忘记关闭这些资源。

Golang中defer关键字的优势在于,在打开资源语句的下一行,就可以直接用defer语句来注册函数结束后执行关闭资源的操作。说白了就是给程序逻辑“上闹钟”,定义好逻辑结束时需要关闭什么资源,如此,就降低了忘记关闭资源的概率:

package main  
  
import (  
    "fmt"  
    "github.com/jinzhu/gorm"  
    _ "github.com/jinzhu/gorm/dialects/mysql"  
)  
  
func main() {  
    db, err := gorm.Open("mysql", "root:root@(localhost)/mytest?charset=utf8mb4&parseTime=True&loc=Local")  
  
    if err != nil {  
                fmt.Println(err)  
               fmt.Println("连接数据库出错")  
        return  
    }  
  
    defer db.Close()  
        fmt.Println("链接Mysql成功")  
  
}

这里通过gorm获取数据库指针变量后,在业务开始之前就使用defer定义好数据库链接的关闭,在main函数执行完毕之前,执行db.Close()方法,所以打印语句是在defer之前执行的。

所以需要注意的是,defer最好在业务前面定义,如果在业务后面定义:

fmt.Println("链接Mysql成功")  
defer db.Close()

这样写就是画蛇添足了,因为本来就是结束前执行,这里再加个defer关键字的意义就不大了,反而会在编译的时候增加程序的判断逻辑,得不偿失。

defer执行顺序问题

Golang并不会限制defer关键字的数量,一个函数中允许多个“延迟任务”:

package main  
  
import "fmt"  
  
func main() {  
    defer func1()  
    defer func2()  
    defer func3()  
}  
  
func func1() {  
    fmt.Println("任务1")  
}  
  
func func2() {  
    fmt.Println("任务2")  
}  
  
func func3() {  
    fmt.Println("任务3")  
}

程序返回:

任务3  
任务2  
任务1

我们可以看到,多个defer的执行顺序其实是“反”着的,先定义的后执行,后定义的先执行,为什么?因为defer的执行逻辑其实是一种“压栈”行为:

package main  
  
import (  
    "fmt"  
    "sync"  
)  
  
// Item the type of the stack  
type Item interface{}  
  
// ItemStack the stack of Items  
type ItemStack struct {  
    items []Item  
    lock  sync.RWMutex  
}  
  
// New creates a new ItemStack  
func NewStack() *ItemStack {  
    s := &ItemStack{}  
    s.items = []Item{}  
    return s  
}  
  
// Pirnt prints all the elements  
func (s *ItemStack) Print() {  
    fmt.Println(s.items)  
}  
  
// Push adds an Item to the top of the stack  
func (s *ItemStack) Push(t Item) {  
    s.lock.Lock()  
    s.lock.Unlock()  
    s.items = append(s.items, t)  
}  
  
// Pop removes an Item from the top of the stack  
func (s *ItemStack) Pop() Item {  
    s.lock.Lock()  
    defer s.lock.Unlock()  
    if len(s.items) == 0 {  
        return nil  
    }  
    item := s.items[len(s.items)-1]  
    s.items = s.items[0 : len(s.items)-1]  
    return item  
}

这里我们使用切片和结构体实现了栈的数据结构,当元素入栈的时候,会进入栈底,后进的会把先进的压住,出栈则是后进的先出:

func main() {  
  
    var stack *ItemStack  
    stack = NewStack()  
    stack.Push("任务1")  
    stack.Push("任务2")  
    stack.Push("任务3")  
    fmt.Println(stack.Pop())  
    fmt.Println(stack.Pop())  
    fmt.Println(stack.Pop())  
  
}

程序返回:

任务3  
任务2  
任务1

所以,在defer执行顺序中,业务上需要先执行的一定要后定义,而业务上后执行的一定要先定义。

除此以外,就是与其他执行关键字的执行顺序问题,比方说return关键字:

package main  
  
import "fmt"  
  
func main() {  
    test()  
}  
  
func test() string {  
    defer fmt.Println("延时任务执行")  
    return testRet()  
}  
  
func testRet() string {  
    fmt.Println("返回值函数执行")  
    return ""  
}

程序返回:

返回值函数执行  
延时任务执行

一般情况下,我们会认为return就是结束逻辑,所以return逻辑应该会最后执行,不是原子操作,在底层分为两步来执行,第一步赋值返回值,第二步进行返回,函数中如果存在defer,那么defer执行的时机是在第一步和第二步之间,也就是defer会在retrun第一步赋值的后面执行。

业务与特性结合

我们知道,有些内置关键字不仅仅具备表层含义,如果了解其特性,是可以参与业务逻辑的,比如说Python中的try{ ...}except{ ...}finally{...}结构,表面上是捕获异常,输出异常,其实可以利用其特性搭配唯一索引,就可以直接完成排重业务,从而减少一次磁盘的IO操作。

defer也如此,假设我们要在同一个函数中打开不同的文件进行操作:

package main  
  
import (  
    "os"  
)  
  
func mergeFile() error {  
  
    f1, _ := os.Open("file1.txt")  
    if f1 != nil {  
  
        //操作文件  
        f1.Close()  
    }  
  
    f2, _ := os.Open("file2.txt")  
    if f2 != nil {  
  
        //操作文件  
        f2.Close()  
    }  
  
    return nil  
}  
  
func main(){  
mergeFile()  
}

所以理论上,需要两个文件句柄对象,分别打开不同的文件,然后同步执行。

但让defer关键字参与进来:

package main  
  
import (  
    "fmt"  
    "io"  
    "os"  
)  
  
func mergeFile() error {  
  
    f, _ := os.Open("file1.txt")  
    if f != nil {  
        defer func(f io.Closer) {  
            if err := f.Close(); err != nil {  
                fmt.Printf("文件1关闭 err %v\n", err)  
            }  
        }(f)  
    }  
  
    f, _ = os.Open("file2.txt")  
    if f != nil {  
        defer func(f io.Closer) {  
            if err := f.Close(); err != nil {  
                fmt.Printf("文件2关闭 err err %v\n", err)  
            }  
        }(f)  
    }  
  
    return nil  
}  
  
func main() {  
  
    mergeFile()  
}

这里就用到了defer的特性,defer函数定义的时候,句柄参数就已经复制进去了,随后,真正执行close()函数的时候就刚好关闭的是对应的文件了,如此,同一个句柄对不同文件进行了复用,我们就节省了一次内存空间的分配。

defer一定会执行吗

我们知道Python中的try{ ...}except{ ...}finally{...}结构,finally仅仅是理论上会执行,一旦遇到特殊情况:

from peewee import MySQLDatabase  
  
class Db:  
  
    def __init__(self):  
  
        self.db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)  
  
    def __enter__(self):  
        print("connect")  
        self.db.connect()  
        exit(-1)  
  
    def __exit__(self,*args):  
        print("close")  
        self.db.close()  
  
with Db() as db:  
    print("db is opening")

程序返回:

connect

并未执行print("db is opening")逻辑,是因为在\_\_enter\_\_方法中就已经结束了(exit(-1))

而defer同理:

package main  
  
import (  
    "fmt"  
    "os"  
)  
  
func main() {  
    defer func() {  
        fmt.Printf("延后执行")  
    }()  
    os.Exit(1)  
}

这里和Python一样,同样调用os包中的Exit函数,程序返回:

exit status 1

延迟方法并未执行,所以defer并非一定会执行。

结语

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

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
4月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
108 4
|
4月前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
61 3
|
2月前
|
存储 设计模式 安全
Go语言中的并发编程:从入门到精通###
本文深入探讨了Go语言中并发编程的核心概念与实践技巧,旨在帮助读者从理论到实战全面掌握Go的并发机制。不同于传统的技术文章摘要,本部分将通过一系列生动的案例和代码示例,直观展示Go语言如何优雅地处理并发任务,提升程序性能与响应速度。无论你是Go语言初学者还是有一定经验的开发者,都能在本文中找到实用的知识与灵感。 ###
|
2月前
|
Serverless Go
Go语言中的并发编程:从入门到精通
本文将深入探讨Go语言中并发编程的核心概念和实践,包括goroutine、channel以及sync包等。通过实例演示如何利用这些工具实现高效的并发处理,同时避免常见的陷阱和错误。
|
3月前
|
安全 Go 开发者
破译Go语言中的并发模式:从入门到精通
在这篇技术性文章中,我们将跳过常规的摘要模式,直接带你进入Go语言的并发世界。你将不会看到枯燥的介绍,而是一段代码的旅程,从Go的并发基础构建块(goroutine和channel)开始,到高级模式的实践应用,我们共同探索如何高效地使用Go来处理并发任务。准备好,让Go带你飞。
|
3月前
|
存储 安全 Go
Go语言切片:从入门到精通的深度探索###
本文深入浅出地剖析了Go语言中切片(Slice)这一核心概念,从其定义、内部结构、基本操作到高级特性与最佳实践,为读者提供了一个全面而深入的理解。通过对比数组,揭示切片的灵活性与高效性,并探讨其在并发编程中的应用优势。本文旨在帮助开发者更好地掌握切片,提升Go语言编程技能。 ###
|
3月前
|
安全 Java Go
【Golang入门】简介与基本语法学习
Golang语言入门教程,介绍了Go语言的简介、基本语法、程序结构、变量和常量、控制结构、函数、并发编程、接口和类型、导入包、作用域以及错误处理等关键概念,为初学者提供了一个全面的学习起点。
128 0
|
5月前
|
Go 开发者
Golang 中的异常处理机制详解
【8月更文挑战第31天】
77 0
|
5月前
|
测试技术 Go 开发者
掌握Golang测试:从入门到实践
【8月更文挑战第31天】
89 0
|
5月前
|
Unix Go
Go从入门到放弃之时间操作
Go从入门到放弃之时间操作