简单先说一下Go的defer特性:
- defer是后进先出的原则执行
- defer是在函数运行结束之后,才会运行
那么这些特性能干什么
- 延迟任务
- 尾部验证(比如运行函数之后,需要对返回值校验)
- 资源销毁
一个具体的场景
在后端开发中,可能需要多人协作,多语言,多模块开发。这时候可能就需要用RPC通信
比如有个用户模块,在进入到一个业务的时候,你需要去验证用户是否合法
async function main(username) {
const client = await rpc.createConnection(); // 创建RPC链接
const data = await client.getUserInfo(username); // 通过RPC链接获取用户信息
// 下面是业务逻辑
}
但是你发现,RPC链接并没有被关闭(即便函数执行完毕,对象client
被销毁), 它依旧占用着服务端的资源,如果多次调用这个函数,会多服务端造成很大的压力.
解决方案有两个
- 维护连接池
- 在每次创建链接之后都销毁
第一种方法是大多数ORM,RPC框架做的,应用内只保持n个链接,不断的创建再销毁,而这个过程不用使用者关心。
很可惜的是,我选用的RPC框架Thrift
,有屎一样的坑。为了保持和Go的业务逻辑一样,采用第二种方案,每次创建链接之后都销毁
,保证它是干净的
依旧是上面的代码,改成这样
async function main(username) {
const client = await rpc.createConnection(); // 创建RPC链接
try{
const data = await client.getUserInfo(username); // 通过RPC链接获取用户信息
// 下面是业务逻辑
}catch (err){
await client.close(); // 关闭链接
throw err
}
}
上面的代码已经能很好的解决问题了,但是也有一个,如果需要关闭多种链接呢(比如RPC,DB,Socket等),一直try,catch也不是办法
受到Go的defer启发,于是写了这么一个库godefer , 依旧改写上面的代码
const godefer = require('godefer');
const main = godefer(async function(username, defer) {
const client = await rpc.createConnection();
// 注册一个defer函数
defer(function() {
client.close(); // 关闭链接
});
const data = await client.getUserInfo(username);
// 下面是业务逻辑
// ...
// 运行完毕,则开始执行defer
});
Defer函数的执行,遵循了后进先出
的原则, 即 先注册的defer函数,后运行
实现上与Go差不多, 有一点重要的差别是:
Go的defer内,能修改父级函数的返回值 godefer不能修改(不是不能做,而是不做)
好了,愉快的解决问题了, 有什么想法,评论来刚啊
突然想到,另一个应用场景。事务
Golang的defer特性,就很好的避免了事务死锁
在defer函数中,根据是否有Error来判断应该是commit还是rollback,再也不用怕忘记了
tj 有类似实现
挺好看的轮子~~楼主让我想到了轮子妈希维尔。。哈哈
业务中的try-catch
该加还是要加的,如果加try-catch
除了释放io资源还有别的意义。
如果只是想将多个逻辑分支中退出后释放io的代码合到一起,用尾部调用挺好的。
尾部调用的话,为啥不用原生的setImmediate
?
退一步说,如果有人catch完还想往外throw,还想释放资源,这时候你的轮子就够不着了,只能老老实实用原生finally
。
@soda-wy try catch不是不行,开发中经常用到的,可是如果需要在事后销毁资源这种情况,需要很多个try catch嵌套,代码可读性会降低
我理解go中的defer
:1尾执行,2无论正常退出还是异常退出都会执行。
你目前实现了1,js中可以用原生setImmediate
或process.nextTick
替代。
你目前没有实现2,js中如果传入的func执行抛异常且没有try-catch
,你的实现并没有接住异常,也就没机会执行事先绑定的资源销毁逻辑。而js原生的try-catch-finally
可以。
setImmediate
我不太确定它的调用时机(一个事件循环结束,是什么时候?不能保证紧跟在这个函数后面调用)
而且调用时机是先进先出
,不符合先进后出
不确定异步情况下的调用顺序
比如
defer(asyncFunc1)
defer(asyncFunc2)
defer(asyncFunc3)
// vs
setImmediate(asyncFunc1)
setImmediate(asyncFunc2)
setImmediate(asyncFunc3)
我可以明确的知道在defer的情况下,执行顺序 asyncFunc3 > asyncFunc2 > asyncFunc1,上一个defer执行完,才执行下一个
setImmediate是怎么执行,顺序怎么样,就不知道了
try catch
如果嵌套几层,就会难看…
基于这个,我把代码里面的try catch
消除了
尝试着引入下真实的业务代码,写进测试分支。
消除了因事务带来的try catch
原代码:
export async function createUser(argv) {
const t = await sequelize.transaction(); // 开启事务
try {
// 业务代码: 创建用户等等其他
// ...
// ...
await t.commit();
} catch (err) {
await t.rollback(); // 一旦捕捉到错误,事务回滚
}
}
// 调用方法
createUser({ name: 'xxx' });
改进之后的代码:
const godefer = require('godefer');
export const createUser = godefer(async function createUser(argv, defer) {
const t = await sequelize.transaction(); // 开启事务
defer(async res => {
if (res instanceof Error) {
await t.rollback(); // 一旦捕捉到错误,事务回滚
} else {
await t.commit();
}
});
// 业务代码: 创建用户等等其他
// ...
// ...
});
// 调用方法
createUser({ name: 'xxx' });
哈哈,我们都是喜欢自己动手实现的人:)这个是我的版本 https://github.com/yyrdl/zco#4-resume–defer
@axetroy 咱一个一个说哈。
1.setImmediate
执行时机。Node里,setImmediate
算是marcoTask, process.nextTick
和Promise
同是microTask。所以如果你觉得setImmediate
执行时机不靠谱,那在用process.nextTick
就跟Promise
一样了。也就是说,如果断了event_loop_tick, 你的godefer和process.nextTick再执行时机是一致的。
2.setImmediate
执行顺序。万一我就是想先进先出
呢?调整调用defer的顺序吗?那同样的调整process.nextTick
不也可以吗?所以我觉得后进先出
并不算啥卖点。。
3.如果func抛了异常。这块怪我之前没说清楚,我的意思是万一我传入的func就是普通的function,不是async function的,且在返回Promise之前就抛了异常,那你的godefer queue就不会执行了。这里你可以在实现上再处理下。
@yyrdl 你的厉害,有好多很有用特性呢
-
…
-
关于
process.nextTick
,如果我开启了一个事物,常理来说,在下一行函数,就要defer取消掉,如果用process.nextTick(commitOrRollbackFunc)
,就不满足。得把这个调用放在最后 -
无论是async function或是普通的function,都能兼容。只是异常理出这个,都没有很好的思路。比方说,主函数执行通过(无报错), 结果在defer函数中抛出错误,那怎么算呢。目前是这样的:如果是主函数运行正常,defer函数抛出错误,那么错误冒泡,也相当于主函数抛出错误。该reject的reject,该throw的throw
1+2 我承认我眼瞎了。
3.我的意思是,主函数是普通函数且异常了,并不会进defer。defer里的异常分场景用finally或冒泡吧。
const godefer = require('godefer');
async function openSomeIO() {
return await Promise.resolve()
}
async function closeSomeIO() {
console.log(`close IO`) //并不会执行到这里释放
}
const createUser = godefer(function createUser(argv, defer) {
openSomeIO();
defer(async res => {
await closeSomeIO()
});
throw new Error(`~~`)
});
createUser({ name: 'xxx' });
@soda-wy 是的,我Get到你的点了,算是Bug吧,只考虑了异步的情况
@axetroy 哈哈,我在自己的项目中发现defer用的很少,但是一到需要的时候就无可替代。