目录

golang-defer机制

defer延迟机制

defer是什么

defer是go中一种延迟调用机制。

执行时机

defer后面的函数只有在当前函数执行完毕后才能执行。

执行顺序

将延迟的语句按defer的逆序进行执行,也就是说先被defer的语句最后被执行,最后被defer的语句,最先被执行,通常用于释放资源。

多个defer本质就是用栈存储,先进后出。

defer定义

go 复制代码
//最后不要忘记函数调用
//使用匿名函数
defer func (形参列表){
    
}(实参)


//最后不要忘记调用函数
//使用事先声明的函数
defer 函数名(实参)

//最后不要忘记调用方法
defer 方法名(形参列表){
    
}(实参)

defer的功能一般是用于释放资源。

defer后面的函数是可以有返回值的,但是一般没有作用。

注意事项

方法或函数必需调用

go 复制代码
//报错:未调用函数
defer func(){
    fmt.Println("a")
}

注意声明顺序

虽然defer的执行时机在函数结束后,但是声明的时候使用的变量或者参数得是函数内在defer声明之前就定义好的。

go 复制代码
//报错:student未定义
defer student.GetName(2)
var student Student

//报错:age未定义
defer func(a int){
    fmt.Printf("年龄为%d\n",age)
}(age)
age := 15

多个defer的执行顺序

多个defer出现的时候,它会把defer之后的函数压入一个栈中延迟执行,也就是先进后出(LIFO).

写在前面的defer会比写在后面的defer调用的晚。下面通过一个示例看一下:

go 复制代码
package defer_knowledge

import "fmt"

type Student struct{
	Name string
}

func (s Student) GetName(n int){
	fmt.Printf("这是第%d个defer\n",n)
}

func sayHello(n int){
	fmt.Printf("这是第%d个defer\n",n)
}

//验证defer的执行顺序
func DeferFirst(){
	fmt.Println("hello world")
	var age int = 25

	defer func (){
		fmt.Println("我是第1个defer")
	}()

	age++
	

	/*
		虽然defer的执行时机在return之后
		但是声明defer时,结构体实例要先声明,否则无法访问结构体实例方法
	*/
	var student Student
	defer student.GetName(2)
	
	defer func (){
		fmt.Println("我是第3个defer")
	}()

	defer sayHello(4)
}

结果

sh 复制代码
这是第4个defer
我是第3个defer
这是第2个defer
我是第1个defer

图示

延迟参数传入时机

基本语法

注意事项

defer函数的入参参数是在defer函数声明时决定的。例如

go 复制代码
package defer_knowledge

import "fmt"

//defer的参数是声明时传入的
func DeferParams(){
	var age = 10
	defer func(a int){
		fmt.Printf("defer内的参数为%d\n",a)
	}(age)

	age = 25

	fmt.Printf("age已经变成了%d\n",age)
}

调用结果

sh 复制代码
age已经变成了25
defer内的参数为10

小结

值类型

所以我们要注意传入的参数,

sh 复制代码
【值类型参数】
值类型参数原始变量改变不影响传入参数,例如int、数组、结构体
如果我们想要defer执行时能读取到变化后的"值类型"参数,可以传入指针

例如

go 复制代码
package defer_knowledge
import "fmt"
//defer的参数是声明时传入的
func DeferParams(){
	var age = 10
	//如果想要追踪值类型的变化可以传入值类型指针
	defer func(a *int){
		fmt.Printf("最初如果传入指针,defer内参数为%d\n",*a)
	}(&age)

	defer func(a int){
		fmt.Printf("defer内的参数为%d\n",a)
	}(age)

	age = 25

	fmt.Printf("age已经变成了%d\n",age)
}

调用结果

sh 复制代码
age已经变成了25
defer内的参数为10
最初如果传入指针,defer内参数为25
引用类型

具体看引用的底层是否发生变换,例如切片,如果没发生扩容将使用相同的。

go 复制代码
package defer_knowledge
import "fmt"
func DeferParams2(){
	var arr = make([]int,5,5)
	//引用类型直接传递即可,将追踪到引用改变为止
	defer func(a []int){
		fmt.Printf("defer内的参数为%#v\n",a)
	}(arr)

	arr[2] = 10
	fmt.Printf("arr已经变成了%#v\n",arr)
}

调用结果

sh 复制代码
arr已经变成了[]int{0, 0, 10, 0, 0}
defer内的参数为[]int{0, 0, 10, 0, 0}

声明时机和执行时机

声明时机

defer的声明时机时按照他出现在代码中的顺序,这时会执行两个操作。

sh 复制代码
1.传入参数
2.检查内部要访问的变量是否已经定义

举例

go 复制代码
func DeferTime(){
    var age int
    /*
    	声明时会传入参数,以及检查内部逻辑是否正确
    */
    defer func(){
        //注意,这个不是defer函数的参数
        //和常规变量作用域一样,本层找不到就去外面找
        age++
    }()
}

错误示范

go 复制代码
func DeferTime(){
    defer func(){
        //报错:age未定义
        age++
    }()
    var age int
}

执行时机

defer的执行时机是在函数逻辑结束后,或者说return后,按照defer栈调用。

举例

go 复制代码
func DeferTime2(){
	var age int 

	defer func(){
		//按照defer栈,此时访问到的age为 11
		age = age+5
		fmt.Printf("age的值为%d\n",age)
	}()

	defer func(){
		//defer执行时机为函数结束后,所以此时访问到的 age = 10
		age++
		fmt.Printf("age的值为%d\n",age)
	}()

	age = 10
}

结果

sh 复制代码
age的值为11
age的值为16

defer与return的区别

图示

可以看到 return 执行的时候,并不是原子性操作,一般是分为两步:将结果x赋值给了返回值,然后执行了RET指令;而defer语句执行的时候,是在赋值变量之后,在RET指令之前。所以这里注意一下。返回值和x的关系。如果x是一个值类型,这里是进行了拷贝的。

执行图示意

函数返回值

不具名返回

形式

go 复制代码
func 函数名(参数列表) 返回值类型{
    return 返回值
}

//例如
/*
	return sum
	操作拆解:
	实际对外暴露返回值为 sum_copy
	sum_copy = sum

	所以return实际执行拷贝操作,他不是将函数内的变量抛出,而是将拷贝后的值抛出
*/
func Add(a,b int) int{
    sum := a+b
    return sum
}

案例1

go 复制代码
func DeferAndReturn1() int{
    var num int
    defer func(){
        num++
        //num的值为16
        fmt.Printf("num的值为%d\n",num)
    }()
    num = 15
    return num
}

//调用
target := DeferAndReturn1()
//target的值为15
fmt.Printf("target的值为%d\n",target)

解析

go 复制代码
func DeferAndReturn1() int{
    var num int
    defer func(){
        num++
        //num的值为16
        fmt.Printf("num的值为%d\n",num)
    }()
    num = 15
    /*
    	实际操作
    	copy_num = num
    	对外暴露copy_num,
    	由于num是值类型,所以后续defer中对num的操作不影响copy_num
    */
    return num
}

误区1

想到了指针操作,但是理解出错。

go 复制代码
func DeferAndReturn1() int{
    var num int
    var ptr = new(int)
    ptr = &num
    defer func(){
        *ptr++
        //num的值为16
        fmt.Printf("num的值为%d\n",num)
    }()
    num = 15
    return num
}
//调用
target := DeferAndReturn1()
//target的值为15
fmt.Printf("target的值为%d\n",target)

原因:

go 复制代码
func DeferAndReturn1() int{
    var num int
    var ptr = new(int)
    ptr = &num
    defer func(){
        *ptr++
        //num的值为16
        fmt.Printf("num的值为%d\n",num)
    }
    num = 15
    /*
    	实际操作
    	copy_num = num
    	对外暴露copy_num,
    	我们修改通过指针ptr修改num的值,还是没影响到copy_num
    */
    return num
}

正确思维

go 复制代码
func DeferAndReturn1() *int{
    var num int
    var ptr = new(int)
    ptr = &num
    defer func(){
        num++
        //num的值为16
        fmt.Printf("num的值为%d\n",num)
    }()
    num = 15
    return ptr
}
//调用
target := DeferAndReturn1()
//target的值为16
fmt.Printf("target的值为%d\n",*target)

结果

sh 复制代码
num的值为16
target的值为16

原因

go 复制代码
func DeferAndReturn1() *int{
    var num int
    var ptr = new(int)
    ptr = &num
    defer func(){
        num++
        //num的值为16
        fmt.Printf("num的值为%d\n",num)
    }()
    num = 15
    /*
    	实际操作
    	copy_ptr = ptr
    	由于ptr是引用类型,所以defer对ptr的影响会影响到copy_ptr
    	
    	num的本质就是*ptr,操作num就是在操作ptr
    */
    return ptr
}
具名返回

相当于实际要暴露的返回值早就确定好了,return只是起到一个结束函数的作用。

go 复制代码
func 函数名(参数列表)(返回值 返回值类型){
    return
}

//示例
/*
	sum就是实际暴露的返回值,且已经声明了
*/
func Add(a,b int) (sum int){
    return
}

案例1

go 复制代码
func DeferAndReturn2() (num int){
	defer func(){
		num++
	}()
	num = 10
	return
}

num2 := DeferAndReturn2()
fmt.Printf("外部num的值为%d\n",num2)

结果

go 复制代码
外部num的值为11

原因

go 复制代码
func DeferAndReturn2() (num int){
	defer func(){
		num++
	}()
	num = 10
    /*
    	这里写return 和 return num一样
    	最终暴露的值为 num
    	所以defer中对num的操作会影响到最终返回值
    */
	return
}
本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
白泽来了6 小时前
2个小时1.5w字| React & Golang 全栈微服务实战
笔记·go·react
柏油6 小时前
MySQL InnoDB 行锁
数据库·后端·mysql
咖啡调调。6 小时前
使用Django框架表单
后端·python·django
白泽talk6 小时前
2个小时1w字| React & Golang 全栈微服务实战
前端·后端·微服务
摆烂工程师6 小时前
全网最详细的5分钟快速申请一个国际 “edu教育邮箱” 的保姆级教程!
前端·后端·程序员
一只叫煤球的猫7 小时前
你真的会用 return 吗?—— 11个值得借鉴的 return 写法
java·后端·代码规范
Asthenia04127 小时前
HTTP调用超时与重试问题分析
后端
颇有几分姿色7 小时前
Spring Boot 读取配置文件的几种方式
java·spring boot·后端
AntBlack7 小时前
别说了别说了 ,Trae 已经在不停优化迭代了
前端·人工智能·后端