原文: https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.4.html

---------------------------------------------------

3.4 defer关键字

defer和go一样都是Go语言提供的关键字。defer用于资源的释放,会在函数返回之前进行调用。一般采用如下模式:

f,err := os.Open(filename)
if err != nil {
panic(err)
}
defer f.Close()

如果有多个defer表达式,调用顺序类似于栈,越后面的defer表达式越先被调用。

不过如果对defer的了解不够深入,使用起来可能会踩到一些坑,尤其是跟带命名的返回参数一起使用时。在讲解defer的实现之前先看一看使用defer容易遇到的问题。

defer使用时的坑

先来看看几个例子。例1:

func f() (result int) {
defer func() {
result++
}()
return 0
}

例2:

func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}

例3:

func f() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}

请读者先不要运行代码,在心里跑一遍结果,然后去验证。

例1的正确答案不是0,例2的正确答案不是10,如果例3的正确答案不是6......

defer是在return之前执行的。这个在 官方文档中是明确说明了的。要使用defer时不踩坑,最重要的一点就是要明白,return xxx这一条语句并不是一条原子指令!

函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。

defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。

其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改写成:

返回值 = xxx
调用defer函数
空的return

先看例1,它可以改写成这样:

func f() (result int) {
result = 0 //return语句不是一条原子调用,return xxx其实是赋值+ret指令
func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间
result++
}()
return
}

所以这个返回值是1。

再看例2,它可以改写成这样:

func f() (r int) {
t := 5
r = t //赋值指令
func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
t = t + 5
}
return //空的return指令
}

所以这个的结果是5。

最后看例3,它改写后变成:

func f() (r int) {
r = 1 //给返回值赋值
func(r int) { //这里改的r是传值传进去的r,不会改变要返回的那个r值
r = r + 5
}(r)
return //空的return
}

所以这个例子的结果是1。

defer确实是在return之前调用的。但表现形式上却可能不像。本质原因是return xxx语句并不是一条原子指令,defer被插入到了赋值 与 ret之间,因此可能有机会改变最终的返回值。

defer的实现

defer关键字的实现跟go关键字很类似,不同的是它调用的是runtime.deferproc而不是runtime.newproc。

在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn。

普通的函数返回时,汇编代码类似:

add xx SP
return

如果其中包含了defer语句,则汇编代码是:

call runtime.deferreturn,
add xx SP
return

goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。

go语言defer关键字背后的实现,语法,用法的更多相关文章

  1. Go 语言入门(一)基础语法

    写在前面 在学习 Go 语言之前,我自己是有一定的 Java 和 C++ 基础的,这篇文章主要是基于A tour of Go编写的,主要是希望记录一下自己的学习历程,加深自己的理解 Go 语言入门(一 ...

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

    先行定义,延后执行.不得不佩服Go lang设计者天才的设计,事实上,defer关键字就相当于Python中的try{ ...}except{ ...}finally{...}结构设计中的finall ...

  3. C语言中关键字auto、static、register、const、volatile、extern的作用

    原文:C语言中关键字auto.static.register.const.volatile.extern的作用 关键字auto.static.register.const.volatile.exter ...

  4. 第二十一节:Java语言基础-关键字,标识符,注释,常量和变量,运算符

    Java语言基础-关键字,标识符,注解,常量和变量,运算符 class Demo { public static void main(String[] args){ System.out.printl ...

  5. go语言 defer 高级

    go语言defer语句的用法 defer的语法 defer后面必须是函数调用语句,不能是其他语句,否则编译器会出错. package main import "log" func ...

  6. C语言void关键字的深刻含义

    C语言void关键字的深刻含义 .概述 本文将对void关键字的深刻含义进行解说,并详述void及void指针类型的使用方法与技巧. .void的含义 void的字面意思是“无类型”,void *则为 ...

  7. Docs-.NET-C#-指南-语言参考-关键字-值类型:enum

    ylbtech-Docs-.NET-C#-指南-语言参考-关键字-值类型:enum 1.返回顶部 1. enum(C# 参考) 2015/07/20 enum 关键字用于声明枚举,一种包含一组被称为枚 ...

  8. Docs-.NET-C#-指南-语言参考-关键字-值类型:bool

    ylbtech-Docs-.NET-C#-指南-语言参考-关键字-值类型:bool 1.返回顶部 1. bool(C# 参考) 2015/07/20 bool 关键字是 System.Boolean  ...

  9. 深入 Go 语言 defer 实现原理

    转载请声明出处哦~,本篇文章发布于luozhiyun的博客: https://www.luozhiyun.com/archives/523 本文使用的go的源码 1.15.7 介绍 defer 执行规 ...

随机推荐

  1. 深入理解linux内核-进程和程序

    进程描述符task_struct task_struct { //进程基本信息 pid 进程id号 tgid 线程组id号,与线程组领头线程pid号相同   getpid()返回该值 tasks in ...

  2. springboot集成elk 一: springboot + Elasticsearch

    1.ELK介绍 1> Elasticsearch是实时全文搜索和分析引擎, 提供搜集.分析.存储数据三大功能: 是一套开放REST和JAVA API等结构提供高效搜索功能,可扩展的分布式系统. ...

  3. Fiddler之手机抓包

    1.Fiddle设置端口,Tools->Options->Connections, 2.手机设置代理服务器: 注意:要保证手机和PC电脑IP在同一个网段(或者同一个网关) (1).安卓手机 ...

  4. oracle管道函数的用法(一行拆为多行)

    oracle管道函数是一类特殊的函数,oracle管道函数返回值类型必须为集合 如果需要在客户端实时的输出函数执行过程中的一些信息,在oracle9i以后可以使用管道函数(pipeline funct ...

  5. ElasticSearch入门-基本概念介绍以及安装

    Elasticsearch基本概念 Elasticsearch是基于Lucene的全文检索库,本质也是存储数据,很多概念与传统关系型数据库类似. 传统关系型数据库与Elasticsearch进行概念对 ...

  6. Feign【token传递】

    使用feign调用服务的时候,存在一个问题,比如当前服务调用A服务,在请求头中包含了某些特殊的字段信息,比如当前操作人的token信息,调用A的时候可以正常拿到token,然而在去调用B服务的时候,可 ...

  7. docker部署Eurake服务,服务节点无法注册服务

    前言 昨天在部署一个docker项目时遇到了一个问题,故记录下来. 环境说明 Centos7 Docker version 18.06.3-ce, build d7080c1 问题说明 该项目分别启动 ...

  8. 怎样理解window.name

    window.name表示当前窗口的名字, 而非网页的名字, 网页的名字需要使用: document.title; window.name一般是空的字符串, 他的作用其实是配合配合超链接和表单的tar ...

  9. (八)CXF之用spring添加拦截器

    一.案例 本章案例是基于CXF之自定义拦截器基础之上改造的,目的是在服务端中用spring添加拦截器 配置web.xml <?xml version="1.0" encodi ...

  10. Android 自定义控件之 日期选择控件

    效果如下: 调用的代码: @OnClick(R.id.btn0) public void btn0() { final AlertDialog dialog = new AlertDialog.Bui ...