打開的東西,就要關掉。這次我講個故事。
假設去圖書館借了全世界只有一本的書。
圖書館員(就是作業系統)告訴你:「你看完之後,必須親手把書還回來,否則下一個人就永遠借不到了。」
這本書,就是一個「資源」——一個檔案、一個網路連線、一個資料庫交易。
系統裡的這些資源數量是有限的。
如果你借了不還,系統會造成資源洩漏 (Resource Leak) ,洩漏幾個沒感覺,洩漏成千上萬個,你的服務就死了。
資源生命週期規則:取得 (Acquire) -> 使用 (Use) -> 保證釋放 (Release)。
重點在「保證」,不管你看書時是睡著了、被隕石砸中、還是看到一半不想看了,你都得把書還回去。
這是初學者寫的程式碼,假設一切順利的流程,借書、讀書、還書。
// 🔴 臭味:這就像說「我看完書一定會還」,但沒考慮任何意外。
async function read(path, fs) {
// 1. 借書 (打開了資源)
const fd = await fs.open(path, 'r');
// 2. 讀書 (使用資源)。如果這本書是空白的或有問題(拋出錯誤)...
const buf = Buffer.alloc(10);
await fs.read(fd, buf, 0, 10, 0);
// 3. ...你就嚇跑了,永遠忘了去還書。底下這行根本沒機會執行。
await fs.close(fd);
return buf;
}
程式碼一旦出錯,它會直接跳過後續所有步驟,包括你那個「還書」的 close()
呼叫。
結果就是,書(資源)永遠留在了你手上,沒人能再用了。
try...finally
try...finally
就是你寫給圖書館的切結書(絕對保證)。
這份try
區塊切結書上寫著:「無論我在讀書時發生什麼事——就算我讀到一半中樂透跑了,或者書太無聊直接提前返回——我finally
區塊裡的程式碼 都會保證執行『還書』這個動作。」
// 🟡 至少是個負責任的人
async function read(path, fs) {
let fd; // 必須把書先拿到外面,切結書才能看到
try {
// 1. 借書
fd = await fs.open(path, 'r');
// 2. 讀書
const buf = Buffer.alloc(10);
await fs.read(fd, buf, 0, 10, 0);
return buf;
} finally {
// 3. 執行切結書:無論如何,只要我借到書(fd存在),就一定還回去!
if (fd) {
await fs.close(fd);
}
}
}
可以這樣用,而且不會毀了你的系統。
但每次借書都要寫一份這麼麻煩的切結書,也容易出錯。
聰明的人會消除犯錯的味道,不會每次都手動,他們會設計一個讓錯誤無法發生的系統,讓還書這個動作自動化。
主要概念是:資源的生命週期綁定在一個物件的生命週期上。
把 try...finally
這個細節封裝起來,變成「借還書機器人」 (withFile
)。
你只要告訴這個機器人你想借哪本書、以及借到書後想做什麼事,剩下的全給它做就好。
這種「將資源的取得與釋放,包裹在一個函式或物件的生命週期內」的技巧,在程式設計領域被稱為「出借模式」(Loan Pattern) 或「環繞執行模式」(Execute Around Method Pattern)。
我們要參考這個精神:讓機器(程式結構)來保證資源的釋放,而不是靠人的記憶力。
// 這個「借還書機器人」,把所有繁瑣的細節都藏在裡面。
async function withFile(path, action) {
let fd;
try {
// 機器人幫你借書
fd = await fs.open(path, 'r');
// 機器人把書交給你,讓你做你想做的事 (action)
return await action(fd);
} finally {
// 你做完事後,機器人保證幫你還書
if (fd) {
await fs.close(fd);
}
}
}
// 🟢 好味道:你只關心讀書的內容,不用擔心還書的細節。
async function read(path, fs) {
// 告訴機器人: 我要這本書(path),拿到後幫我執行這段讀書計畫。
return withFile(path, async (fd) => {
const buf = Buffer.alloc(10);
await fs.read(fd, buf, 0, 10, 0);
return buf;
});
}
用 withFile
時,根本沒有機會忘記還書,物件生命週期結束時 (離開了作用域),它會被自動銷毀,此時就釋放資源(還書)。
這個將資源取得與釋放包裹起來的技巧,把資源生命週期管理這個複雜問題,抽象成一個簡單、可靠的工具。
await using
)「出借模式」的精神非常棒,現代 JavaScript 甚至將這個模式內建到了語言本身。using
的用法是:離開作用域時,可以使用 Symbol.dispose
釋放掉任何內容。參考連結:using
而用於非同步資源則是 await using
。參考連結:await using
要使用這個特性,我們只需要讓資源物件符合可棄置資源(Disposable Resource) 規範,也就是提供一個名為 [Symbol.asyncDispose]
的清理方法。
// 建立一個會回傳「可自動關閉檔案」物件的函式
async function openFileResource(path, fs) {
const fileHandle = await fs.open(path, 'r');
console.log("檔案已開啟...");
return {
handle: fileHandle,
// [規範核心] 定義非同步的清理方法
[Symbol.asyncDispose]: async () => {
await fileHandle.close();
console.log("檔案已透過 asyncDispose 自動關閉!");
}
};
}
// 🟢🟢 頂級好味道:語法即保證,簡潔又安全
async function readModern(path, fs) {
// 使用 await using 取得資源,語言會記住它需要被釋放
await using file = await openFileResource(path, fs);
// 專注於核心邏輯
const buf = Buffer.alloc(10);
await file.handle.read(buf, 0, 10, 0);
return buf;
// 完全不需要 try...finally 或 close() 呼叫!
// 這才是真正的「自動除臭」
}
當 file
變數離開作用域時,無論是正常回傳還是中途出錯,[Symbol.asyncDispose]
方法都會被自動呼叫並等待完成。
實現了「打開的東西,就要關掉,沒有例外」的最高境界。
手動保證:最低標準是養成使用 try...finally
的習慣。雖然繁瑣,但它能確保在各種情況下執行必要的清理工作。
模式抽象:比手動更好的是,將邏輯封裝成可重用的輔助函式,也就是「出借模式」(範例withFile
),能將「取得」和「保證釋放」的流程自動化。
語法內建:最現代且可靠的方式,是利用 JavaScript 語言內建的 using
和 await using
。讓語法本身成為你的「自動除臭器」。
目標是盡可能地使用高層次抽象,讓機器(程式結構或語言本身)來保證資源的釋放,而不是依賴人的記憶力。
try...finally
是手動確保資源釋放的基礎作法。using
或 await using
,讓語法為你自動管理資源的生命週期,這是最安全、最簡潔的選擇。一個好的程式設計師不是每次都記得防臭,而是發明了「自動除臭」。