尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

踩了这些坑,我 才真正搞懂Go的 `defer`,真坑啊

踩了这些坑,我 才真正搞懂Go的 `defer`,真坑啊 实话实说defer是我学 Go 时最早接触的几个特性之一但也是我花时间真正搞懂它最晚的一个。刚开始觉得它就是“函数返回时执行”挺简单的。直到线上出过几次事故我才发现这玩意儿的坑比我想象的多得多。下面这几个坑都是我在实际生产环境里踩过的或者亲眼看着同事踩过的。1. 参数是当场算的不是最后才算这是defer最阴险的地方。你以为defer里的参数会等到函数退出时才取值错了它在你写defer那一行就把参数算好了。funcprocessOrder(idstring){status:pendingdeferfmt.Printf(order %s status: %s\n,id,status)statuspaid// 中间一堆业务逻辑}你猜最后打印出来的是啥是pending不是paid。因为defer那一行执行的时候status的值还是pending这个值已经被复制进去了。后面的修改跟它没关系了。怎么修用闭包funcprocessOrder(idstring){status:pendingdeferfunc(){fmt.Printf(order %s status: %s\n,id,status)}()statuspaid}闭包捕获的是变量本身而不是当前值所以最后打印出来的是paid。这个细节是很多defer相关 bug 的根源。2. 循环里用defer资源会堆到函数结束才释放很多新手包括当年的我会在循环里这么写funcprocessFiles(ids[]string)error{for_,id:rangeids{f,err:os.Open(file_id.txt)iferr!nil{returnerr}deferf.Close()// 每循环一次就堆一个 Close 在这里// 处理文件内容...}returnnil}循环 10 次没问题100 次也还好。但如果是 1 万次呢每个defer f.Close()都不会在循环结束的时候执行而是要等整个processFiles函数返回。于是你同时打开了 1 万个文件操作系统直接给你报错too many open files。正确的做法是把循环体拆成一个独立的函数funcprocessFiles(ids[]string)error{for_,id:rangeids{iferr:processOneFile(id);err!nil{returnerr}}returnnil}funcprocessOneFile(idstring)error{f,err:os.Open(file_id.txt)iferr!nil{returnerr}deferf.Close()// 处理文件内容...returnnil}这样每处理一个文件defer就执行一次资源及时释放。3. 别把Close()的返回值扔了很多人写defer f.Close()的时候压根不管它返回什么错误。大部分时候确实没事但万一有事呢funcwriteAuditLog(entrystring)error{f,err:os.Create(audit.log)iferr!nil{returnerr}deferf.Close()_,errf.WriteString(entry)returnerr}如果f.WriteString成功了但f.Close()因为磁盘满了或者其他原因失败了你永远不知道——因为你把Close的返回值忽略了。如果你真的在意这个错误写审计日志应该在意可以这么写funcwriteAuditLog(entrystring)(errerror){f,err:os.Create(audit.log)iferr!nil{returnerr}deferfunc(){ifcerr:f.Close();cerr!nilerrnil{errcerr}}()_,errf.WriteString(entry)returnerr}注意那个err nil的判断。它的意思是如果之前已经有错误了就别用Close的错误把前面的错误覆盖掉。不然你可能会看到一个“文件关闭失败”的错误但真正的问题其实是“磁盘满了”。4. 锁一直不解搞半天是因为defer的范围太大了defer mu.Unlock()是一个很常见的写法大部分时候没问题。但如果你的函数在释放锁之后还有一堆不需要锁的操作问题就来了func(s*Store)UpdatePrice(idstring,pricefloat64)error{s.mu.Lock()defers.mu.Unlock()s.items[id].Priceprice// 保存到数据库...// 发通知给其他服务...// 写审计日志...// 这些都是网络或磁盘操作不需要锁returnnil}锁一直挂到函数结束而那些耗时的网络请求、磁盘写入都在锁的保护范围内这会严重影响并发性能。解法很简单把需要锁的代码包在一个小范围里而不是整个函数。func(s*Store)UpdatePrice(idstring,pricefloat64)error{// 加锁更新放锁func(){s.mu.Lock()defers.mu.Unlock()s.items[id].Priceprice}()// 不需要锁的操作放到外面// 保存到数据库...// 发通知...returnnil}5.recover()只能救当前 goroutine很多人都知道defer recover()可以捕获 panic但很容易忽略一个细节它只能在同一个 goroutine 里生效。funcmain(){deferfunc(){ifr:recover();r!nil{fmt.Println(caught:,r)}}()gofunc(){panic(boom!)// 这个 panic 不会被上面的 recover 捕获}()time.Sleep(time.Second)}上面的recover捕获不到子 goroutine 的 panic因为那是不同的 goroutine。每个 goroutine 都要有自己的defer recover保护。如果你在工作池里启动了很多 goroutine只要有一个 panic 了而没被捕获整个进程就没了。说到底defer不是“随便一放就行”上面这些 bug 都有一个共同点把defer当作“放那就不用管了”的清理工具而忽略了它其实是一个有明确求值规则和调用时机的函数调用。只要你记住两个问题参数什么时候算—— 是执行defer的时候不是函数返回的时候。调用什么时候执行—— 是函数返回前但不是循环结束前也不是锁的作用域结束前。defer本身是个好东西是 Go 里最优雅的清理机制之一。但它的“简单”是骗人的——那些你没注意的细节最后都会出现在事故报告里。
返回列表