延宕执行,妙用无穷,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中defer关键字延迟调用机制使用EP17
先行定义,延后执行。不得不佩服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会在retrun后面执行,所以defer中的逻辑如果依赖return中的执行结果,那么就绝对不能使用defer关键字。
业务与特性结合
我们知道,有些内置关键字不仅仅具备表层含义,如果了解其特性,是可以参与业务逻辑的,比如说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关键字的一种致敬罢。
延宕执行,妙用无穷,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中defer关键字延迟调用机制使用EP17的更多相关文章
- 仙人指路,引而不发,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中New和Make函数的使用背景和区别EP16
Golang只有二十五个系统保留关键字,二十几个系统内置函数,加起来只有五十个左右需要记住的关键字,纵观编程宇宙,无人能出其右.其中还有一些保留关键字属于"锦上添花",什么叫锦上添 ...
- 清源正本,鉴往知来,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中引用类型是否进行引用传递EP18
开篇明义,Go lang中从来就不存在所谓的"引用传递",从来就只有一种变量传递方式,那就是值传递.因为引用传递的前提是存在"引用变量",但是Go lang中从 ...
- 百亿数据百亿花, 库若恒河沙复沙,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang数据库操作实践EP12
Golang可以通过Gorm包来操作数据库,所谓ORM,即Object Relational Mapping(数据关系映射),说白了就是通过模式化的语法来操作数据库的行对象或者表对象,对比相对灵活繁复 ...
- 你有对象类,我有结构体,Go lang1.18入门精炼教程,由白丁入鸿儒,go lang结构体(struct)的使用EP06
再续前文,在面向对象层面,Python做到了超神:万物皆为对象,而Ruby,则干脆就是神:飞花摘叶皆可对象.二者都提供对象类操作以及继承的方式为面向对象张目,但Go lang显然有一些特立独行,因为它 ...
- 兔起鹘落全端涵盖,Go lang1.18入门精炼教程,由白丁入鸿儒,全平台(Sublime 4)Go lang开发环境搭建EP00
Go lang,为并发而生的静态语言,源于C语言又不拘泥于性能,高效却不流于古板,Python灵活,略输性能,Java严谨,稍逊风骚.君不见各大厂牌均纷纷使用Go lang对自己的高并发业务进行重构, ...
- 化整为零优化重用,Go lang1.18入门精炼教程,由白丁入鸿儒,go lang函数的定义和使用EP07
函数是基于功能或者逻辑进行聚合的可复用的代码块.将一些复杂的.冗长的代码抽离封装成多个代码片段,即函数,有助于提高代码逻辑的可读性和可维护性.不同于Python,由于 Go lang是编译型语言,编译 ...
- 因势而变,因时而动,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang泛型(generic)的使用EP15
事实上,泛型才是Go lang1.18最具特色的所在,但为什么我们一定要拖到后面才去探讨泛型?类比的话,我们可以想象一下给小学一年级的学生讲王勃的千古名篇<滕王阁序>,小学生有多大的概率可 ...
- 层次分明井然有条,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang包管理机制(package)EP10
Go lang使用包(package)这种概念元素来统筹代码,所有代码功能上的可调用性都定义在包这个级别,如果我们需要调用依赖,那就"导包"就行了,无论是内部的还是外部的,使用im ...
- 并发与并行,同步和异步,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang并发编程之GoroutineEP13
如果说Go lang是静态语言中的皇冠,那么,Goroutine就是并发编程方式中的钻石.Goroutine是Go语言设计体系中最核心的精华,它非常轻量,一个 Goroutine 只占几 KB,并且这 ...
随机推荐
- 聊聊 RPA 方向的规划:简单有价值的事情长期坚持做
「简单有价值的事情长期坚持做」 这是成功最简单,但也最难学的秘诀.不经过训练,人很难意识到时间复利的威力. 仙剑奇侠传的「十里坡剑神」和金庸群侠传的「十级野球拳」,就是简单的事情持之以恒反复做,最后就 ...
- js 表面学习 - 认识函数
JavaScript 函数语法 JavaScript 函数通过 function 关键词进行定义,其后是函数名和括号 (). 函数名可包含字母.数字.下划线和美元符号(规则与变量名相同). 圆括号可包 ...
- python小题目练习(八)
题目:电视剧的收视率排行榜 需求:实现如下图所示需求 代码展示: """Author:mllContent:电视剧的收视率排行榜Date:2020-11-16" ...
- NC16660 [NOIP2004]FBI树
NC16660 [NOIP2004]FBI树 题目 题目描述 我们可以把由"0"和"1"组成的字符串分为三类:全"0"串称为B串,全&quo ...
- 基于.NetCore开发博客项目 StarBlog - (14) 实现主题切换功能
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- ReentrantLock 公平锁源码 第1篇
ReentrantLock 1 这篇还是接着ReentrantLock的公平锁,没看过第0篇的可以先去看上一篇https://www.cnblogs.com/sunankang/p/16456342. ...
- samba打开一个文件的函数调用栈
ceph_open cephwrap_open open_fn smb_vfs_call_open SMB_VFS_OPEN fd_open fd_open_atomic open_file open ...
- Nginx工作模式
Master-Worker模式 1.Nginx 在启动后,会有一个 master 进程和多个相互独立的 worker 进程.2.接收来自外界的信号,向各worker进程发送信号,每个进程都有可能来处理 ...
- 【每天学一点-01】 在SpringBoot项目中使用Swagger2
今天在做毕设的时候,发现在前后端分离的情况下,去调用接口数据时很不方便,然后回想过去,和同学一起做项目的时候,他负责后端,我负责前端,当时调用他的弄好的接口可以说是非常方便,主要是可以通过UI页面直接 ...
- appache ab测试高并发
ab使用范例: 要执行 1000 次的 connection, 20 次的 concurrent (并行, 同时): 语法: ab -n 1000 -c 20 http://localhost/ind ...