工程趣聞 2026 年 10 月 11 日

2026-10-11 — 把 Go 的 defer 塞進 typescript-go,作者卻說不該上線

primary=https://healeycodes.com/adding-defer-to-the-typescript-compiler primary=https://github.com/healeycodes/typescript-go/tree/defer-v1 primary=https://github.com/tc39/proposal-explicit-resource-management primary=https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management

有人把 Go 的 defer 塞進 typescript-go,作者做完卻說不該上線

healeycodes.com(Andrew Healey) · 2026 年 8 月

TypeScript 想把「取得資源、馬上登記釋放」寫成一行 defer sema.release();,取代今天的 try/finally 巢狀寫法。Andrew Healey 在 typescript-go(Go 版 tsc)上做出了可運作的 MVP,並公開在他的 fork 分支 defer-v1。

有趣的是,他的結論是 defer 不該進 TypeScript,真正該用的是 TC39 的明確資源管理提案。這篇文章等於一份「為什麼這條路走不通」的實作紀錄。

原本的問題

在 TypeScript 裡,凡是要成對處理的資源(鎖、semaphore、檔案),都得把後續邏輯包進 try,再於 finally 釋放。取得與釋放被拉開,中間的程式碼越長,越容易漏掉或寫錯順序。

現在defer 版
寫法try { ... } finally { sema.release(); }defer sema.release();
釋放位置函式尾端緊接在取得之後

作者選 tsc 當實驗場,是因為它雖以靜態分析為主,卻早就有語法對語法的改寫機制,例如把 ?. 降級成條件運算式、為 ES5 降級 class field。所以 defer 只需要新增一個 AST 節點與一個 transform。

核心改動

解析與檢查:parser 新增 DeferStatement 語法種類,運算元只有一個運算式。checkDeferStatement 要求它必須位於有區塊本體的函式內、不能在 generator 內,且運算元(去掉括號後)必須是呼叫運算式,否則報錯。

降級方式:每個函式建立一個 closure 堆疊,defer 執行時 push,最後在 finally 反向清空。輸出骨架大致如下(註解已精簡):

const stack = [];
try {
  await sema.acquire();
  const receiver = sema;
  const method = receiver.release;
  stack.push(() => method.call(receiver));
  // ... 函式本體
} catch (error) { /* 保存原始錯誤 */ }
finally { /* 反向執行,async 內逐一 await */ }

語意上有三個決定:呼叫目標、receiver 與參數在執行到 defer 時就先求值,之後改了變數或重新指派 logger.log 都不影響;defer 只有執行流經過才登記,放在迴圈內就每圈登記一次;清理順序是 LIFO。作者也刻意拒絕 defer await,避免使用者誤以為 await 會在函式其餘部分之前完成。

錯誤處理才是卡關處

Go 的 deferred 呼叫回傳的錯誤只是普通值,呼叫者可以忽略。但 JavaScript 的例外是控制流,降級後必須回答:本體丟錯、清理又丟錯,要保留哪一個?

  • 每個 deferred 呼叫都要執行,即使前一個丟出例外。
  • 本體的原始錯誤要保留。
  • 多個失敗以 AggregateError 回報,例如本體丟錯且 asyncCleanup() 也 reject 時,f() 會 reject 成包含兩者的 AggregateError。

作者認為這套優先順序與聚合規則是語言層級的政策負擔,屬於設計上的壞味道。文章未提供任何效能數字或基準測試,也沒有說明 transform 的完整程式碼(他說太冗長而省略)。

影響範圍

這不是一個會讓你的 build 壞掉的變更,目前也不是任何 TypeScript 發行版的功能,只是實驗分支。文章未提到有對應的 TypeScript 官方 PR。對一般專案的實際意義在於替代方案:

  • 用 semaphore、lock、檔案 handle 且寫了大量 try/finally 的程式碼,可改用 TypeScript 5.2 起支援的 using 宣告,由 TC39 Explicit Resource Management 提案定義。
  • 要套用的資源物件需自行提供可被 using 釋放的包裝,作者範例是 acquirePermit(sema)。
async function readFile(path: string) {
    using _ = await acquirePermit(sema);
    return await fsReadFile(path, "utf8");
}

作者說他更想要 using await acquirePermit(sema); 這種寫法,但該語法目前不被支援。另外,MVP 的限制包括:運算元只能是呼叫運算式、不可用於 generator、deferred 呼叫的回傳值未處理。

如果你正在寫自己的語法降級或 codemod,這篇的價值在於它具體列出「eager capture、LIFO、錯誤聚合」三個必須先決定的語意,而不是 Go 那種單純的 defer 語法糖。

原始來源:Adding Go's defer to the TypeScript Compiler、defer-v1 分支、TC39 Explicit Resource Management


End of article
0
Would love your thoughts, please comment.x
()
x