前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >延宕执行,妙用无穷,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中defer关键字延迟调用机制使用EP17

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

作者头像
用户9127725
发布2022-09-26 16:08:39
2000
发布2022-09-26 16:08:39
举报
文章被收录于专栏:刘悦的技术博客

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

    使用场景

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

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

代码语言:javascript
复制
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最好在业务前面定义,如果在业务后面定义:

代码语言:javascript
复制
fmt.Println("链接Mysql成功")
defer db.Close()

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

    defer执行顺序问题

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

代码语言:javascript
复制
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")
}

    程序返回:

代码语言:javascript
复制
任务3
任务2
任务1

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

代码语言:javascript
复制
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
}

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

代码语言:javascript
复制
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())

}

    程序返回:

代码语言:javascript
复制
任务3
任务2
任务1

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

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

代码语言:javascript
复制
package main

import "fmt"

func main() {
	test()
}

func test() string {
	defer fmt.Println("延时任务执行")
	return testRet()
}

func testRet() string {
	fmt.Println("返回值函数执行")
	return ""
}

    程序返回:

代码语言:javascript
复制
返回值函数执行
延时任务执行

    一般情况下,我们会认为return就是结束逻辑,所以return逻辑应该会最后执行,但实际上defer会在retrun后面执行,所以defer中的逻辑如果依赖return中的执行结果,那么就绝对不能使用defer关键字。

    业务与特性结合

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

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

代码语言:javascript
复制
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关键字参与进来:

代码语言:javascript
复制
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仅仅是理论上会执行,一旦遇到特殊情况:

代码语言:javascript
复制
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")

    程序返回:

代码语言:javascript
复制
connect

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

    而defer同理:

代码语言:javascript
复制
package main

import (
	"fmt"
	"os"
)

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

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

代码语言:javascript
复制
exit status 1

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

    结语

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-08-26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  •     使用场景
  •     defer执行顺序问题
  •     业务与特性结合
  •     defer一定会执行吗
  •     结语
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档