开发者生态
morning
将 Go 的 Defer 添加到 TypeScript 编译器
摘要
I wanted to see how difficult it would be to add Go's defer statement to the TypeScript compiler, but by the time I finished I was convinced it probably shouldn't exist. In Go, the defer statement del...
defer
the
TypeScript
like
function
statement
sema
compiler
resource
and
2026-08-03
1 阅读
约9分钟阅读
healeycodes
字号:
我想看看将 Go 的 defer 语句添加到 TypeScript 编译器中会有多困难,但当我完成时,我确信它可能不应该存在。在 Go 中,defer 语句延迟函数的执行,直到周围的函数完成。它最常用于将资源获取和清理结合在一起,例如获取信号量: func withSemaphore ( ctx context . Context , sem * semaphore . Weighted ) error { if err := sem . Weighted 。获取(ctx,1); err != nil { return err } 推迟 sem 。 Release (1) // ... protected work return nil } TypeScript 没有严格等效的 defer 。您可以使用 try / finally ,例如: async function readFile( path : string ) { wait sema .获得 ( ) ; try { // ... 使用资源 } 最后 { sema .发布 ( ) ;但这有点难看。为了好玩,我们可以向 TypeScript 编译器添加 defer 语句并获得类似 Go 的语义。由于 defer 不映射到现有的 JavaScript 功能,因此我们需要输出 JavaScript 代码,使其在运行时工作,就像在 Go 中一样。因此,我们的目标是能够编写如下 TypeScript 代码: async function readFile(path: string) { wait sema 。获得 ( ) ;推迟塞玛。发布 ( ) ; // 新的! // ... 使用资源 } TypeScript 编译器 TypeScript 编译器 ( tsc ) 主要是一个静态分析引擎。它的复杂性在于对基本动态语言进行类型检查,并支持极度增量编译以满足 IDE 中的延迟预期。对我们来说幸运的是,我们不需要太多担心类型或其他分析来添加我们的 defer 语句。 tsc 已经拥有“识别语法 X,用等效语法 Y 替换它”的机制。例如,当编译 ES5 时: 可能会变成这样: function Foo ( ) { this . x = 1 ;从概念上讲,添加 defer 意味着进行另一次树重写。 tsc 已经执行了许多 AST 到 AST 转换(例如可选链接 ?. 成为条件表达式),因此我们不需要添加新工具。有一些复杂性需要深入研究,但在较高的层次上,我们将采用带有 defer 的 AST: function f ( ) { defer cleanup ( ) ;工作 ( ) ;并将其转换为: function f ( ) { const __defers = [ ] ;尝试{__defers。推送 ( ( ) => 清理 ( ) ) ;工作 ( ) ; } finally { // Pop 并调用 } } 首先,我们需要告诉 tsc 的解析器 defer 是一个语句。我们向其中添加了 DeferStatement 的语法类型列表,并将其定义为采用单个表达式操作数。我们需要执行一些检查,例如确保 defer 语句出现在函数体内,确保表达式可调用,并确保 tsc 执行其通常的递归检查: func ( c * Checker ) checkDeferStatement ( node * ast . Node ) { c . checkGrammarStatementInAmbientContext (node) // defer 与其包含函数的生命周期相关联 fn := ast 。 GetContainingFunction ( 节点 ) if fn == nil || fn 。正文 ( ) == nil || ! AST。 IsBlock(fn.Body()){c. GrammarErrorOnNode( 节点,诊断。Defer_statements_can_only_be_used_inside_function_bodies) } else if ast 。 GetFunctionFlags ( fn ) 和 ast 。 FunctionFlagsGenerator != 0 { c . GrammarErrorOnNode ( node , Diagnostics . Defer_statements_cannot_be_used_in_generators ) } // 仅支持调用,这样可以保持捕获/降低明确的表达式 := ast 。 SkipParentheses (node.Expression()) if ! AST。 IsCallExpression ( 表达式 ) { c . GrammarErrorOnNode( 节点 . 表达式 () , 诊断 . The_operand_of_a_defer_statement_must_be_a_call_expression ) c. checkExpression ( node . Expression ( ) ) return } // 重用普通调用检查(可调用被调用者、参数类型等) c . checkExpression ( expression ) } 实际的转换代码非常冗长,因此我不会在这里重现它,而是深入研究我所做的设计决策,并告诉您有关转换如何工作的更多信息。我认为 defer 应该如何工作 为了匹配 Go 的行为,被调用者、接收者和参数值会被立即捕获:let x = 1 ;推迟控制台。日志(x); x = 2 ;它必须打印 1 。我们需要生存的一个极端情况是重新定义可调用方法,例如: const logger = { log ( message : string ) { console 。日志(“旧:”,消息); } , } ;延迟记录器。日志(“你好”); // 延迟记录器之后一切都会改变。日志 = ( 消息 ) => { 控制台 .日志(“新:”,消息); };即使稍后重新分配 logger.log,延迟的调用仍然会调用原始方法。这符合 Go 的语义,即当执行到达 defer 语句时,函数值、接收者和参数都会被求值。任何至少包含一个 defer 的函数都会获得一个小堆栈,并且每个到达的 defer 语句都会将一个闭包推送到该堆栈上。当函数退出时,堆栈将以相反的顺序(后进先出)清空。注册发生在执行到达延迟时,而不是函数启动时。 S
这篇文章对您有帮助吗?
订阅66必读
每日精选科技资讯,直达你的邮箱