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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 先行定义,延后执行。不得不佩服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关键字的一种致敬罢。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
相关文章
|
1月前
|
Java 编译器 Go
【Golang】(1)Go的运行流程步骤与包的概念
初次上手Go语言!先来了解它的运行流程吧! 在Go中对包的概念又有怎样不同的见解呢?
88 4
|
1月前
|
Java 编译器 Go
【Golang】(5)Go基础的进阶知识!带你认识迭代器与类型以及声明并使用接口与泛型!
好烦好烦好烦!你是否还在为弄不懂Go中的泛型和接口而烦恼?是否还在苦恼思考迭代器的运行方式和意义?本篇文章将带你了解Go的接口与泛型,还有迭代器的使用,附送类型断言的解释
112 3
|
1月前
|
存储 安全 Java
【Golang】(4)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法
结构体可以存储一组不同类型的数据,是一种符合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
92 1
|
4月前
|
人工智能 测试技术 持续交付
Golang深入浅出之-Go语言中的持续集成与持续部署(CI/CD)
持续集成与持续部署(CI/CD)是现代软件开发的关键实践,尤其适用于Go语言项目。本文探讨了Go项目中常见的CI/CD问题,如测试覆盖不足、版本不一致和构建时间过长,并提供解决方案及GitHub Actions示例代码,帮助开发者优化流程,提升交付效率和质量。
125 5
|
6月前
|
Go 调度
GO语言函数的内部运行机制分析
以上就是Go语言中函数的内部运行机制的概述,展示了函数在Go语言编程中如何发挥作用,以及Go如何使用简洁高效的设计,使得代码更简单,更有逻辑性,更易于理解和维护。尽管这些内容深入了一些底层的概念,但我希望通过这种方式,将这些理论知识更生动、更形象地带给你,让你在理解的同时找到编程的乐趣。
93 5
|
6月前
|
安全 Go
defer关键字:延迟调用机制-《Go语言实战指南》
Go 语言中的 `defer` 是用于延迟执行函数调用的关键字,广泛应用于资源释放、异常捕获和日志记录等场景。它在函数返回前执行,支持栈式后进先出(LIFO)顺序,参数求值时机为声明时而非执行时。常见用法包括文件关闭、锁解锁及结合 `recover` 处理 panic。尽管高效,频繁使用可能带来性能开销,需谨慎处理。总结而言,`defer` 是构建健壮代码的核心工具之一。
|
8月前
|
Go 开发者
go-carbon v2.6.0 重大版本更新,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 Golang 时间处理库,提供了对时间穿越、时间差值、时间极值、时间判断、星座、星座、农历、儒略日 / 简化儒略日、波斯历 / 伊朗历的支持
149 3
|
7月前
|
存储 算法 物联网
解析局域网内控制电脑机制:基于 Go 语言链表算法的隐秘通信技术探究
数字化办公与物联网蓬勃发展的时代背景下,局域网内计算机控制已成为提升工作效率、达成设备协同管理的重要途径。无论是企业远程办公时的设备统一调度,还是智能家居系统中多设备间的联动控制,高效的数据传输与管理机制均构成实现局域网内计算机控制功能的核心要素。本文将深入探究 Go 语言中的链表数据结构,剖析其在局域网内计算机控制过程中,如何达成数据的有序存储与高效传输,并通过完整的 Go 语言代码示例展示其应用流程。
135 0
|
12月前
|
JSON Go 开发者
go-carbon v2.5.0 发布,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 Golang 时间处理库,提供了对时间穿越、时间差值、时间极值、时间判断、星座、星座、农历、儒略日 / 简化儒略日、波斯历 / 伊朗历的支持。
238 4
|
12月前
|
存储 安全 Go
Go 语言以其高效的并发编程能力著称,主要依赖于 goroutines 和 channels 两大核心机制
Go 语言以其高效的并发编程能力著称,主要依赖于 goroutines 和 channels 两大核心机制。本文介绍了这两者的概念、用法及如何结合使用,实现任务的高效并发执行与数据的安全传递,强调了并发编程中的注意事项,旨在帮助开发者更好地掌握 Go 语言的并发编程技巧。
141 2

推荐镜像

更多