golang中defer的使用规则详解

前言

在golang当中,defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用。因此,defer通常用来释放函数内部变量。

为了更好的学习defer的行为,我们首先来看下面一段代码:

func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}

dst, err := os.Create(dstName)
if err != nil {
return
}

written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}

这段代码可以运行,但存在'安全隐患'。如果调用dst, err := os.Create(dstName)失败,则函数会执行return退出运行。但之前创建的src(文件句柄)没有被释放。 上面这段代码很简单,所以我们可以一眼看出存在文件未被释放的问题。 如果我们的逻辑复杂或者代码调用过多时,这样的错误未必会被及时发现。 而使用defer则可以避免这种情况的发生,下面是使用defer的代码:

func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()

dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()

return io.Copy(dst, src)
}

通过defer,我们可以在代码中优雅的关闭/清理代码中所使用的变量。defer作为golang清理变量的特性,有其独有且明确的行为。以下是defer三条使用规则。

规则一 当defer被声明时,其参数就会被实时解析

我们通过以下代码来解释这条规则:

func a() {
i := 0
defer fmt.Println(i)
i++
return
}

上面我们说过,defer函数会在return之后被调用。那么这段函数执行完之后,是不用应该输出1呢?

读者自行编译看一下,结果输出的是0. why?

这是因为虽然我们在defer后面定义的是一个带变量的函数: fmt.Println(i) . 但这个变量(i)在defer被声明的时候,就已经确定其确定的值了。 换言之,上面的代码等同于下面的代码:

func a() {
i := 0
defer fmt.Println(0) //因为i=0,所以此时就明确告诉golang在程序退出时,执行输出0的操作
i++
return
}

为了更为明确的说明这个问题,我们继续定义一个defer:

func a() {
i := 0
defer fmt.Println(i) //输出0,因为i此时就是0
i++
defer fmt.Println(i) //输出1,因为i此时就是1
return
}

通过运行结果,可以看到defer输出的值,就是定义时的值。而不是defer真正执行时的变量值(很重要,搞不清楚的话就会产生于预期不一致的结果)

但为什么是先输出1,在输出0呢? 看下面的规则二。

规则二 defer执行顺序为先进后出

当同时定义了多个defer代码块时,golang安装先定义后执行的顺序依次调用defer。不要为什么,golang就是这么定义的。我们用下面的代码加深记忆和理解:

func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}

在循环中,依次定义了四个defer代码块。结合规则一,我们可以明确得知每个defer代码块应该输出什么值。 安装先进后出的原则,我们可以看到依次输出了3210.

规则三 defer可以读取有名返回值

先看下面的代码:

func c() (i int) {
defer func() { i++ }()
return 1
}

输出结果是12. 在开头的时候,我们说过defer是在return调用之后才执行的。 这里需要明确的是defer代码块的作用域仍然在函数之内,结合上面的函数也就是说,defer的作用域仍然在c函数之内。因此defer仍然可以读取c函数内的变量(如果无法读取函数内变量,那又如何进行变量清除呢....)。

当执行return 1 之后,i的值就是1. 此时此刻,defer代码块开始执行,对i进行自增操作。 因此输出2.

掌握了defer以上三条使用规则,那么当我们遇到defer代码块时,就可以明确得知defer的预期结果。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Golang巧用defer进行错误处理的方法

    本文主要跟大家介绍了Golang巧用defer进行错误处理的相关内容,分享出来供大家参考学习,下面来看看详细的介绍: 问题引入 毫无疑问,错误处理是程序的重要组成部分,有效且优雅的处理错误是大多数程序员的追求.很多程序员都有C/C++的编程背景,Golang的程序员也不例外,他们处理错误有意无意的带着C/C++的烙印. 我们看看下面的例子,就有一种似曾相识的赶脚,代码如下: func deferDemo() error { err := createResource1() if err != n

  • 总结Go语言中defer的使用和注意要点

    前言 defer是golang语言中的关键字,用于资源的释放,会在函数返回之前进行调用. 一般采用如下模式: f,err := os.Open(filename) if err != nil { panic(err) } defer f.Close() 如果有多个defer表达式,调用顺序类似于栈,越后面的defer表达式越先被调用. 延时调用函数的语法如下: defer func_name(param-list) 当一个函数调用前有关键字 defer 时, 那么这个函数的执行会推迟到包含这个

  • Go语言中的延迟函数defer示例详解

    前言 大家都知道go语言的defer功能很强大,对于资源管理非常方便,但是如果没用好,也会有陷阱哦.Go 语言中延迟函数 defer 充当着 try...catch 的重任,使用起来也非常简便,然而在实际应用中,很多 gopher 并没有真正搞明白 defer.return.返回值.panic 之间的执行顺序,从而掉进坑中,今天我们就来揭开它的神秘面纱!话不多说了,来一起看看详细的介绍吧. 先来运行下面两段代码: A. 匿名返回值的情况 package main import ( "fmt&qu

  • golang中defer的关键特性示例详解

    前言 大家都知道golang的defer关键字,它可以在函数返回前执行一些操作,最常用的就是打开一个资源(例如一个文件.数据库连接等)时就用defer延迟关闭改资源,以免引起内存泄漏.本文主要给大家介绍了关于golang中defer的关键特性,分享出来供大家参考学习,下面话不多说,来一起看看详细的介绍: 一.defer 的作用和执行时机 go 的 defer 语句是用来延迟执行函数的,而且延迟发生在调用函数 return 之后,比如 func a() int { defer b() return

  • GO语言Defer用法实例分析

    本文实例讲述了GO语言Defer用法.分享给大家供大家参考.具体分析如下: defer:调用一个被 defer 的函数时在函数刚要返回之前延迟执行,当函数无论怎样返回,某资源必须释放时,可用这种与众不同.但有效的处理方式.传统的例子包括解锁互斥或关闭文件. 这样延迟一个函数有双重优势:一是你永远不会忘记关闭文件,此错误在你事后编辑函数添加一个返回路径时常常发生.二是关闭和打开靠在一起,比放在函数尾要清晰很多. 复制代码 代码如下: /**  * Created with IntelliJ IDE

  • GO语言延迟函数defer用法分析

    本文实例讲述了GO语言延迟函数defer用法.分享给大家供大家参考.具体分析如下: defer 在声明时不会立即执行,而是在函数 return 后,再按照 FILO (先进后出)的原则依次执行每一个 defer,一般用于异常处理.释放资源.清理数据.记录日志等.这有点像面向对象语言的析构函数,优雅又简洁,是 Golang 的亮点之一. 代码1:了解 defer 的执行顺序 复制代码 代码如下: package main import "fmt" func fn(n int) int {

  • golang中defer的使用规则详解

    前言 在golang当中,defer代码块会在函数调用链表中增加一个函数调用.这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用.因此,defer通常用来释放函数内部变量. 为了更好的学习defer的行为,我们首先来看下面一段代码: func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) if err != nil {

  • python golang中grpc 使用示例代码详解

    python 1.使用前准备,安装这三个库 pip install grpcio pip install protobuf pip install grpcio_tools 2.建立一个proto文件hello.proto // [python quickstart](https://grpc.io/docs/quickstart/python.html#run-a-grpc-application) // python -m grpc_tools.protoc --python_out=. -

  • 前端Vue中常用rules校验规则详解

    目录 1.是否合法IP地址 2.是否手机号码或者固话 3.是否固话 4.是否手机号码 5.是否身份证号码 6.是否邮箱 7.合法url 8.验证内容是否包含英文数字以及下划线 9.自动检验数值的范围 10.验证数字输入框最大数值 11.验证是否1-99之间 12.验证是否整数 13.验证是否整数,非必填 14. 验证是否是[0-1]的小数 15. 验证是否是[1-10]的小数,即不可以等于0 16.验证是否是[1-100]的小数,即不可以等于0 17.验证是否是[0-100]的小数 18.验证端

  • Golang 中反射的应用实例详解

    目录 引言 Golang类型设计原则 Golang 中为什么要使用反射/什么场景可以(应该)使用反射 举例场景: 反射的基本用法 反射的性能分析与优缺点 测试反射结构体初始化 测试结构体字段读取/赋值 测试结构体方法调用 优缺点 反射在 okr 中的简单应用 结论 引言 首先来一段简单的代码逻辑热身,下面的代码大家觉得应该会打印什么呢? type OKR struct { id int content string } func getOkrDetail(ctx context.Context,

  • WPF中NameScope的查找规则详解

    前言 我们在 WPF 中使用绑定时可以使用 ElementName=Foo 这样的写法,并且还能够真的在运行时找到这个名称对应的对象,是因为 WPF 中提供了名称范围概念. 实现 INameScope 接口可以定义一个名称范围.无论你使用 Name 属性还是使用 x:Name 特性都可以在一个名称范围内指定某个元素的名称.绑定时就在此名称范围内查找,于是可以找到你需要的对象. XAML中的NameScope 首先来讲讲WPF的名称管理机制NameScope,也即是名称范围.名称范围主要提供了两种

  • 在 Golang 中实现 Cache::remember 方法详解

    项目需要把部分代码移植到 Golang , 之前用 Laravel 封装的写起来很舒服,在 Golang 里只能自动动手实现. 一开始想的是使用 interface 实现,但是遇到了一个坑, Golang 里的组合是一个虚假的继承 package main import "fmt" type Person interface { Say() Name() } type Parent struct { } func (s *Parent) Say() { fmt.Println(&quo

  • golang中的空接口使用详解

    1.空接口 Golang 中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口.空接口表示,没有任何约束,因此任何类型变量都可以实现空接口.空接口在实际项目中用的是非常多的,用空接口可以表示任意数据类型 func main() { // 定义一个空接口 x, x 变量可以接收任意的数据类型 var x interface{} s := "你好 golang" x = s fmt.Printf("type:%T value:%v\n", x, x) i :=

  • seajs中模块的解析规则详解和模块使用总结

    seajs github 模块标识已经说的相对清楚了.但并没有面面俱到,特别是当你需要手写 [模块ID]和[模块依赖]的时候,或者自己写自动化工具来做 transport 的时候(ps:spm貌似适应性不是很强也不易用,毕竟每个项目的目录结构可能相差很大,且不易改变.当然如果他的定位是包管理工具就别指望它来做你的项目的自动化构建工具了),ID的解析规则就需要了解透彻了.注意事项:1. 顶级标识始终相对 base 基础路径解析.2. 绝对路径和根路径始终相对当前页面解析.3. require 和

  • Angular.JS中指令的命名规则详解

    命名规范 同一个AngularJS指令,在js文件和html文件中有着不同的命名规范:在js文件中使用标准的小驼峰命名法,在html文件中使用"小写字母+连接符"的命名法.如下表所示 在js文件中 在html文件中 ngApp ng-app myDirective my-directive 处理机制 AngularJS之所以选择这样的命名方式,是因为html文件不区分大小写,而js文件则对大小写敏感(myDir和mydir在js文件中是不同的指令,但html看来是同一个指令),为了避免

  • Oracle中分组查询group by用法规则详解

    Oracle中group by用法 在select 语句中可以使用group by 子句将行划分成较小的组,一旦使用分组后select操作的对象变为各个分组后的数据,使用聚组函数返回的是每一个组的汇总信息. 使用having子句 限制返回的结果集.group by 子句可以将查询结果分组,并返回行的汇总信息Oracle 按照group by 子句中指定的表达式的值分组查询结果. 在带有group by 子句的查询语句中,在select 列表中指定的列要么是group by 子句中指定的列,要么包

随机推荐