Golang defer的Javascript实现
发布于 6 年前 作者 axetroy 5157 次浏览 来自 分享

简单先说一下Go的defer特性:

  • defer是后进先出的原则执行
  • defer是在函数运行结束之后,才会运行

那么这些特性能干什么

  • 延迟任务
  • 尾部验证(比如运行函数之后,需要对返回值校验)
  • 资源销毁

一个具体的场景

在后端开发中,可能需要多人协作,多语言,多模块开发。这时候可能就需要用RPC通信

比如有个用户模块,在进入到一个业务的时候,你需要去验证用户是否合法

async function main(username) {
  const client = await rpc.createConnection(); // 创建RPC链接
  const data = await client.getUserInfo(username); // 通过RPC链接获取用户信息
  // 下面是业务逻辑
}

但是你发现,RPC链接并没有被关闭(即便函数执行完毕,对象client被销毁), 它依旧占用着服务端的资源,如果多次调用这个函数,会多服务端造成很大的压力.

解决方案有两个

  1. 维护连接池
  2. 在每次创建链接之后都销毁

第一种方法是大多数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不能修改(不是不能做,而是不做)

好了,愉快的解决问题了, 有什么想法,评论来刚啊

14 回复

突然想到,另一个应用场景。事务

Golang的defer特性,就很好的避免了事务死锁

在defer函数中,根据是否有Error来判断应该是commit还是rollback,再也不用怕忘记了

挺好看的轮子~~楼主让我想到了轮子妈希维尔。。哈哈


业务中的try-catch该加还是要加的,如果加try-catch除了释放io资源还有别的意义。

如果只是想将多个逻辑分支中退出后释放io的代码合到一起,用尾部调用挺好的

尾部调用的话,为啥不用原生的setImmediate

退一步说,如果有人catch完还想往外throw,还想释放资源,这时候你的轮子就够不着了,只能老老实实用原生finally

@soda-wy try catch不是不行,开发中经常用到的,可是如果需要在事后销毁资源这种情况,需要很多个try catch嵌套,代码可读性会降低

@axetroy

我理解go中的defer:1尾执行,2无论正常退出还是异常退出都会执行。 你目前实现了1,js中可以用原生setImmediateprocess.nextTick替代。 你目前没有实现2,js中如果传入的func执行抛异常且没有try-catch,你的实现并没有接住异常,也就没机会执行事先绑定的资源销毁逻辑。而js原生的try-catch-finally可以。

@soda-wy

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.nextTickPromise同是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 你的厉害,有好多很有用特性呢

  1. 关于process.nextTick,如果我开启了一个事物,常理来说,在下一行函数,就要defer取消掉,如果用process.nextTick(commitOrRollbackFunc),就不满足。得把这个调用放在最后

  2. 无论是async function或是普通的function,都能兼容。只是异常理出这个,都没有很好的思路。比方说,主函数执行通过(无报错), 结果在defer函数中抛出错误,那怎么算呢。目前是这样的:如果是主函数运行正常,defer函数抛出错误,那么错误冒泡,也相当于主函数抛出错误。该reject的reject,该throw的throw

@axetroy

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用的很少,但是一到需要的时候就无可替代。

回到顶部