通过本节的学习以避免掉入基础的 defer 陷阱中
本文只适合想要进阶学习 Golang 的新手阅读,大牛请绕道。
如果一个延迟函数被赋值为 nil
, 运行时的 panic
异常会发生在外围函数执行结束后而不是 defer
的函数被调用的时候。
例子
func() {
var run func() = nil
defer run()
fmt.Println("runs")
}
输出结果
runs
❗️ panic: runtime error: invalid memory address or nil pointer dereference
名为 func 的函数一直运行至结束,然后 defer
函数会被执行且会因为值为 nil
而产生 panic
异常。然而值得注意的是,run()
的声明是没有问题,因为在外围函数运行完成后它才会被调用。
上面只是一个简单的案例,但同样的案例也可能发生在真实世界中,所以如果你遇上的话,可以想想是不是掉进了这个坑里。
切忌在循环中使用 defer
,除非你清楚自己在做什么,因为它们的执行结果常常会出人意料。
但是,在某些情况下,在循环中使用 defer
会相当方便,例如将函数中的递归转交给 defer
,但这显然已经不是本文应该讲解的内容。
在上面的例子中,defer row.Close()
在循环中的延迟函数会在函数结束过后运行,而不是每次 for 循环结束之后。这些延迟函数会不停地堆积到延迟调用栈中,最终可能会导致一些不可预知的问题。
不使用 defer
,直接在末尾调用。
将任务转交给另一个函数然后在里面使用 defer
,在下面这种情况下,延迟函数会在每次匿名函数执行结束后执行。
有时出于某种缘由,你想要让那些闭包延迟执行。例如,连接数据库,然后在查询语句执行过后中断与数据库的连接。
例子
type database struct{}
func (db *database) connect() (disconnect func()) {
fmt.Println("connect")
return func() {
fmt.Println("disconnect")
}
}
运行一下
db := &database{}
defer db.connect()
fmt.Println("query db...")
输出结果
query db...
connect
最终 disconnect
并没有输出,最后只有 connect
,这是一个 bug,最终的情况是 connect()
执行结束后,其执行域得以被保存起来,但内部的闭包并不会被执行。
func() {
db := &database{}
close := db.connect()
defer close()
fmt.Println("query db...")
}
稍作修改后, db.connect()
返回了一个函数,然后我们再对这个函数使用 defer
就能够在 func()
执行结束后断开与数据库的连接。
输出结果
connect
query db...
disconnect
即便这种处理方式很糟,但我还是想告诉你如何不用变量来解决这个问题,因此,我希望你能以此来了解 defer 亦或是 go 语言的运行机制。
func() {
db := &database{}
defer db.connect()()
..
}
这段代码从技术层面上说与上面的解决方案没有本质区别。其中,第一个圆括号是连接数据库(在 defer db.connect()
中立即执行的部分),然后第二个圆括号是为了在 func()
结束时延迟执行断开连接的函数(也就是返回的闭包)。
归因于 db.connect()
创建了一个闭包类型的值,然后再使用 defer
声明闭包函数, db.connect()
的值需要被实现计算出来以便让 defer
知道需要延迟哪个函数,这与 defer
不直接相关但也可能帮助你解决一些问题。
你可能想要在执行块执行结束后执行在块内延迟调用的函数,但事实并非如此,它们只会在块所属的函数执行结束后才被执行,这种情况适用于所有的代码块除了上文的函数块例如,for,switch 等。
因为:延迟是相对于一个函数而非一个代码块
例子
func main() {
{
defer func() {
fmt.Println("block: defer runs")
}()
fmt.Println("block: ends")
}
fmt.Println("main: ends")
}
输出结果
block: ends
main: ends
block: defer runs
上例的延迟函数只会在函数执行结束后运行,而不是紧接着它所在的块(花括号内包含 defer 调用的区域)后执行,就像代码中的演示的那样,你可以使用花括号创造单独的执行块。
如果你希望在另一个块中使用 defer
,可以使用匿名函数(正如在第二个坑中我们采用的解决方案)。
func main() {
func() {
defer func() {
fmt.Println("func: defer runs")
}()
fmt.Println("func: ends")
}()
fmt.Println("main: ends")
}
同样,你也可以使用 defer
来延迟 方法 调用,但也可能出一些岔子。
type Car struct {
model string
}
func (c Car) PrintModel() {
fmt.Println(c.model)
}
func main() {
c := Car{model: "DeLorean DMC-12"}
defer c.PrintModel()
c.model = "Chevrolet Impala"
}
输出结果
DeLorean DMC-12
func (c *Car) PrintModel() {
fmt.Println(c.model)
}
输出结果
Chevrolet Impala
我们需要记住的是,当外围函数还没有返回的时候,Go 的运行时就会立刻将传递给延迟函数的参数保存起来。
因此,当一个以值作为接收者的方法被 defer 修饰时,接收者会在声明时被拷贝(在这个例子中那就是 Car 对象),此时任何对拷贝的修改都将不可见(例中的 Car.model ),因为,接收者也同时是输入的参数,当使用 defer 修饰时会立刻得出参数的值(也就是 "DeLorean DMC-12" )。
在另一种情况下,当被延迟调用时,接收者为指针对象,此时虽然会产生新的指针变量,但其指向的地址依然与上例中的 "c" 指针的地址相同。因此,任何修改都会完美地作用在同一个对象中。
以上就是本文的全部内容,我会在后续的文章中补充更多类似的坑 -- 已经有至少 15 个易犯的 defer 错误榜上有名,如果你有任何想法,欢迎在下面留言。
via: https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01
作者:Inanc Gumus 译者:yujiahaol68 校对:rxcai
本文由 GCTT 原创翻译,Go语言中文网 首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT!
翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。
欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。
文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽
Original url: Access
Created at: 2019-05-23 18:04:24
Category: default
Tags: none
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
java windows火焰图_mob64ca12ec8020的技术博客_51CTO博客 - 在windows下不可行,不知道作者是怎样搞的 监听SpringBoot 服务启动成功事件并打印信息_监听springboot启动完毕-CSDN博客 SpringBoot中就绪探针和存活探针_management.endpoint.health.probes.enabled-CSDN博客 u2u转换板 - 嘉立创EDA开源硬件平台 Spring Boot 项目的轻量级 HTTP 客户端 retrofit 框架,快来试试它!_Java精选-CSDN博客 手把手教你打造一套最牛的知识笔记管理系统! - 知乎 - 想法有重合-理论可参考 安宇雨 闲鱼 机械键盘 客制化 开贴记录 文本 linux 使用find命令查找包含某字符串的文件_beijihukk的博客-CSDN博客_find 查找字符串 ---- mac 也适用 安宇雨 打字音 记录集合 B站 bilibili 自行搭建 开坑 真正的客制化 安宇雨 黑苹果开坑 查找工具包maven pom 引用地 工具网站 Dantelis 介绍的玩轴入坑攻略 --- 关于轴的一些说法 --- 非官方 ---- 心得而已 --- 长期开坑更新 [本人问题][新开坑位]关于自动化测试的工具与平台应用 机械键盘 开团 网站记录 -- 能做一个收集的程序就好了 不过现在没时间 -- 信息大多是在群里发的 - 你要让垃圾佬 都去一个地方看难度也是很大的 精神支柱 [超级前台]sprinbboot maven superdesk-app 记录 [信息有用] [环境准备] [基本完成] [sebp/elk] 给已创建的Docker容器增加新的端口映射 - qq_30599553的博客 - CSDN博客 [正在研究] Elasticsearch, Logstash, Kibana (ELK) Docker image documentation elasticsearch centos 安装记录 及 启动手记 正式服务器 39 elasticsearch 问题合集 不断更新 6.1.1 | 6.5.1 两个版本 博客程序 - 测试 - bug记录 等等问题 laravel的启动过程解析 - lpfuture - 博客园 OAuth2 Server PHP 用 Laravel 搭建带 OAuth2 验证的 RESTful 服务 | Laravel China 社区 - 高品质的 Laravel 和 PHP 开发者社区 利用Laravel 搭建oauth2 API接口 附 Unauthenticated 解决办法 - 煮茶的博客 - SegmentFault 思否 使用 OAuth2-Server-php 搭建 OAuth2 Server - 午时的海 - 博客园 基于PHP构建OAuth 2.0 服务端 认证平台 - Endv - 博客园 Laravel 的 Artisan 命令行工具 Laravel 的文件系统和云存储功能集成 浅谈Chromium中的设计模式--终--Observer模式 浅谈Chromium中的设计模式--二--pre/post和Delegate模式 浅谈Chromium中的设计模式--一--Chromium中模块分层和进程模型 DeepMind 4 Hacking Yourself README.md update 20211011
Laravel China 简书 知乎 博客园 CSDN博客 开源中国 Go Further Ryan是菜鸟 | LNMP技术栈笔记 云栖社区-阿里云 Netflix技术博客 Techie Delight Linkedin技术博客 Dropbox技术博客 Facebook技术博客 淘宝中间件团队 美团技术博客 360技术博客 古巷博客 - 一个专注于分享的不正常博客 软件测试知识传播 - 测试窝 有赞技术团队 阮一峰 语雀 静觅丨崔庆才的个人博客 软件测试从业者综合能力提升 - isTester IBM Java 开发 使用开放 Java 生态系统开发现代应用程序 pengdai 一个强大的博主 HTML5资源教程 | 分享HTML5开发资源和开发教程 蘑菇博客 - 专注于技术分享的博客平台 个人博客-leapMie 流星007 CSDN博客 - 舍其小伙伴 稀土掘金 Go 技术论坛 | Golang / Go 语言中国知识社区
最新评论