本文发表于入职啦(公众号: ruzhila) 大家可以访问入职啦学习更多的编程实战。
任何时候,写精简的代码都是对自己的一种挑战,也是对自己的一种提升
案例背景
今天群里有人发了一段代码,让大家看看这段代码有什么问题:
这个代码的功能是希望在指定的时间内得到ntp服务器的时间,如果超时,就返回一个错误。
因为ntp查询可能会比较长时间,所以这里使用了启动一个goroutine来执行query,然后通过chan来通知主goroutine;
主goroutine会等待返回结果,并且启动一个定时器,如果超时,就返回一个错误
问题分析
其实这个代码是有问题,我一开始也没注意到,就关注了代码是否写得精简,所以我很快给出了一个解决方案:
通过查看github.com/beevik/ntp的代码,除了Query,还提供了一个高级接口:QueryWithOptions:
也就是默认情况下,不需要你自己处理超时的问题,这个库已经帮你处理好了,所以我们可以直接使用这个接口,我的建议代码如下:
ntp.QueryWithOptions(ntpServer, ntp.QueryOptions{Timeout: 1 * time.Second})
那么这样的情况,就完全不需要启动一个协程去处理超时的问题了,这样代码就变得更加简洁了。
真正的问题
我以为这样就可以了,后来群里的高手提醒说goroutine泄露,之前代码goroutine没有被正确的关闭,这个时候我才意识到,这个问题的根本原因是因为goroutine没有被正确的关闭。
问题就出在第6行:
ntpQState = make(chan bool)
这个ntpQState是一个无缓冲的chan, 如果没有缓存的情况下,会导致发送方一直阻塞等待,那么当出现超时之后,还没结果的情况下,第19行执行了超时逻辑之后,离开这个函数
这时候即便goroutine执行结束之后,在第11和13行的情况下,都会导致这个操作呗阻塞,导致了goroutine泄露。
非常重要的一个结论:
当你用定时器+chan的方式去做超时处理的时候,一定要记得在超时之后,关闭chan,或者预留一个buffer,不要让发送方一直阻塞等待
解决方案
很快群里的人发了一个解决方案:
针对发送者可能会出现因为没有缓冲导致block, golang也有一个内置方案,就是判断是否有缓冲:
select {
case ntpQState <- true:
default:
}
当没有缓冲的时候就可以解决这个问题了, 所以导致goroutine泄露的问题就解决了
开始精简代码
明白chan的无缓冲的带来危害之后,我们开始真正的精简代码:
相比原始代码,这个代码更加简洁:
- 所有的代码不需要单独先声明,可以用 := 来声明
- 不需要用time.NewTimer,用time.After来替代
- 函数的返回值应该是(bool, error),这样可以知晓是超时,还是其他错误
- chan 的内容变成了error
这个代码其实还不是最精简的代码,回到一开始的代码用法:
简单明了,少了一个goroutine,代码更加简洁,也更加易读
总结
bug数量和代码数量成正比关系,这是一直强调的观点,在任何时候,少写代码能避免很多问题。
当然对自己使用的语言,特性一定要深入去学习了了解,不然到处埋坑,大部分时间都在修复bug,得不偿失
最后感谢群里提出改进意见的同学,让我学到了很多,也希望大家多多交流,共同进步
我们构建了一个100行代码项目的实战群,大家可以扫码加入,一起学习编程
也可以访问入职啦学习更多的编程实战