goto-cleanup 模式(the goto-cleanup pattern)
/ goto: "GOH-too" /
想像離開一棟有好幾道門的建築,但規矩是大家都從同一道後門出去,那裡的燈光、警報和門鎖全在同一處理完。你不會在每道門各自關燈;你走向那唯一的出口,關機程序就在那裡正確地、每次都發生。goto-cleanup 模式把這個想法套用到 C 函式上:不在每個提早 return 處散落清理碼,而是跳到底部單一的清理區段,在那裡釋放資源。
具體來說,你在函式末端標上若干標籤,並用 C 的 goto 在失敗時跳到它們。隨著你取得資源,你把清理標籤排成取得順序的反序,這樣跳到正確的標籤就能恰好回復到目前為止取得的東西。典型骨架:開檔;若後面的 malloc() 失敗,goto close_file;若更後面的步驟失敗,goto free_buf(它接著會落穿到 close_file)。清理標籤是疊起來的,每個各做自己的釋放、然後落穿進下一個,以取得的反序釋放各項資源。這正是你會看到 goto——平常不鼓勵使用——在慣用 C 中被使用、甚至被推薦的主因,包括整個 Linux 核心都是如此。它直接解決了清理問題:每項資源都只有一處釋放它,所以很容易驗證沒有洩漏、也沒有被釋放兩次。
為何重要:C 沒有自動清理、沒有解構子、沒有「finally」區塊。少了這個模式,錯誤處理就退化成層層巢狀的 if-else,或在每個 return 處複製貼上的清理碼,而那正是重複釋放與洩漏滋生之處。誠實的提醒:goto-cleanup 是對 goto 有紀律、有結構的使用(永遠向前跳、只跳到清理標籤)——它不是寫義大利麵式亂跳的許可證。而且它需要小心:變數必須在第一個可能的 goto 之前初始化為安全的「尚未取得任何東西」的值(指標設 NULL、描述符設 -1),這樣清理碼才能判斷哪些真的取得了,並跳過釋放那些沒取得的。
int f(void) { int rc = -1; char *buf = NULL; int fd = open("x", O_RDONLY); if (fd < 0) goto out; buf = malloc(1024); if (buf == NULL) goto close_fd; /* ... 做事 ...; rc = 0; */ free(buf); close_fd: close(fd); out: return rc; }——這些標籤以取得的反序釋放,而每個提早退出都會跳過釋放它從未取得的東西。
底部一條清理鏈;在符合你走到多遠的那個點跳進去。
在任何 goto 之前,把資源變數初始化成「尚未取得」的哨符(NULL、-1),否則清理碼可能釋放一個垃圾指標、或關閉描述符 0。goto-cleanup 是有結構的(只向前跳、跳到標籤);它不是讓你到處亂跳的邀請函。