第二部分:大分裂:我们如何学会停止忧虑并爱上结构
欢迎来到我们旅程的第二部分。在第一部分中,我们为你的旧世界观举行了一场光荣的葬礼。我们证明了行号和 PEEK/POKE 不仅仅是“原始”的怪癖,它们是在严苛硬件限制下诞生的、合乎逻辑的天才产物1。你,这位“恐龙”程序员,并非原始人;你是一位在资源匮牲时代精打细算的艺术家。
但现在,我们要谈谈为什么那个世界必须消亡。
你所熟知的世界充满了自由。你可以随意 POKE 内存,可以用 GOTO 指挥程序执行流像舞池里的探戈一样(或者更像喝醉了的布朗运动)在代码间穿梭。这种完全的、不受约束的自由,在构建一个 100 行的太空入侵者游戏时,是一种力量;但在构建一个 100 万行(或 1 亿行)的银行系统时,它就成了一场灾难。
为了建造“代码山”上的摩天大楼,我们不能再像游牧民族一样随心所欲地搭建帐篷。我们需要蓝图、规则、以及一种叫做“结构”的无聊东西。本部分就是关于这场“大分裂”的血腥历史:一场终结了牛仔编码时代的哲学革命。我们将从一场(被标题党点燃的)学术圣战开始,这场战争的焦点,就是你工具箱里那个最强大、也最危险的工具。
第三章:去死吧,GOTO(以及为什么我们只对了一半)
亲爱的时空旅行者,欢迎回到这个奇怪的新世界。在你沉睡的这段时间里,你工具箱里那个最可靠、最基础的命令——GOTO——已经被公开处决了。不仅如此,它还被钉在了历史的耻辱柱上,成为了“糟糕编程”的代名词。
你一定很困惑。在你的时代,GOTO 根本不是“邪恶”的;它就是……呃,控制流2。没有它,你怎么写 IF...THEN 逻辑?你怎么构建循环?你怎么从子程序里跳出来3?
你醒来后,发现一群年轻的程序员(他们可能从没见过电传打字机4)用看史前巨兽的眼神看着你,因为你居然想“无条件地转移控制权”。这个世界到底发生了什么?
这场战争不是关于一个关键词。这是一场关于编程“哲学”的圣战。它有原罪,有先知,有异端,还有一部(被严重误读的)圣经。而你,我亲爱的朋友,是时候补上这段缺失的历史了。
“原罪”:一盘名叫“意大利面条”的地狱美食
GOTO 的问题,正如你的大纲(一本好书!)所指出的那样,不在于它无法工作1。见鬼,它的问题在于它太能工作了。它赋予了你一种神圣的、无限制的权力:在任何时候,从代码的任何地方,跳转到任何其他地方。
在你的 50 行 BASIC 程序里,这很方便。
10 PRINT "GUESS A NUMBER (1-10): ";
20 INPUT G
30 IF G = 7 THEN GOTO 100
40 PRINT "WRONG! TRY AGAIN!"
50 GOTO 10
100 PRINT "YOU GOT IT!"
110 END
看,多清晰!
现在,请想象一下,这不是 11 行,而是 5000 行。想象这是由六个不同的程序员在三年时间里写成的,他们都已经离职了。想象第 40 行的 GOTO 目标不是第 10 行,而是第 4370 行。而第 4370 行的代码在一个 IF 语句里,可能会 GOTO 到第 220 行。而第 220 行又会 GOSUB 到第 3000 行,后者在满足某个条件时 GOTO 回第 20 行。
你试图在脑海中追踪这段逻辑。你拿出一支笔,在打印出来的代码上画线,试图连接这些跳转。五分钟后,你桌上的纸看起来就像一碗被诅咒的意大利面条5。
这就是“意大利面条式代码”(Spaghetti Code)1。它不是一个比喻;它是一种诊断。它描述的是一种精神上的痛苦——那种当你试图理解一个程序的执行流程,却发现自己陷入了一个由 GOTO 织成的、永无止境的逻辑迷宫时的恐怖感。
我们不是在开玩笑。编程界的“恐怖故事”专栏里堆满了这种代码酿成的惨剧。
- 恐怖故事一:没有函数的俄罗斯方块。 一位程序员(为了忏悔?)曾分享过他的杰作:他用 C++ 写了一个完整的俄罗斯方塊游戏,所有代码——游戏逻辑、计分、方块旋转、渲染——全部塞在
main()函数里。没有一个单独的函数调用。所有的控制流都由goto和几层深不见底的缩进构成6。你能想象调试它吗?“嘿,当 Z 型方块旋转时游戏崩溃了。”“好的,让我看看main()里的第 14,000 行……” - 恐怖故事二:吞噬开发者的 osCommerce。 在 PHP 的黑暗时代,有一个叫 osCommerce 的电商平台。它功能强大,而且是“开源”的——这意味着你可以亲眼目睹那地狱般的代码7。一位可怜的开发者在论坛上讲述了他的经历:他接手了一个基于 osCommerce 的网站,客户要求做一些本地化修改。他打开了源代码。那是一片由
include和goto(以及 PHP 版本的 goto:require_once)构成的原始丛林。文本字符串(“Add to Cart”)有些在语言文件里,有些硬编码在函数里,还有些藏在数据库的某个角落。这位开发者写道:“我开始明白为什么前一个开发者会凭空消失了。”7 - 恐怖故事三:“雪花”的悲剧。 这个故事最能触及“原罪”的核心8。每个程序员的职业生涯都是从编写“完美的小雪花”开始的。这是一个你私下里写的脚本,它只做一件事,而且做得特别好。它的变量名清晰,逻辑如诗篇般优美。你甚至会“在夜深人静时,关掉灯,倒上一杯苏格兰威士忌,一边听着德国电子乐,一边欣赏它。”8
然后,你加入了公司。
现实是:销售团队在周五下午告诉你,“我们需要在下周二之前交付六百片雪花。”8 于是,你开始作弊。你复制粘贴,你到处加goto。你把你的雪花和同事(那个刚融化了他自己雪花的人)的雪花粘在一起。最后,“所有程序员的雪花都被倾倒在一起,形成某种难以名状的形状”,然后“有人靠在它上面放了一幅毕加索的画,因为没人想看到那堆在日光下融化、浸泡在猫尿里的、破碎的雪花。”8
这就是意大利面条。它不是被设计出来的;它是在现实的压力下堆积而成的。而 GOTO,就是那个让这一切成为可能的、最快的工具。它太好用了,好用到了灾难性的地步1。
“先知”艾兹格·迪杰斯特拉(Edsger Dijkstra)与一场(被标题党)点燃的圣战
当一个行业被“原罪”所困扰时,它就需要一位“先知”。我们的先知来了,他就是艾兹格·迪杰斯特拉(Edsger Dijkstra)。
迪杰斯特拉是一位计算机科学界的泰坦。他不是那种“搞定就行”的工程师;他是一位用纯数学的严谨来看待编程的哲学家和逻辑学家9。对他来说,程序不应该是“一堆指令”,而应该是一个“可被证明的数学定理”。
1968 年,迪杰斯特拉对这种意大利面条式的混乱状态感到忍无可忍,他给《ACM 通讯》(Communications of the ACM)投了一篇稿件10。
现在,请注意:这个故事最精彩的部分,大多数程序员都不知道。
一场由“标题党”引发的血案
- “温和的”原文:迪杰斯特拉的原稿标题是什么?是那个充满火焰和硫磺的著名标题吗?不。他的原标题是:"A Case Against the Goto Statement"(“一份反对 goto 语句的案例陈词”)11。这是一个温和的、学术的、供人商榷的标题。
- “搞事的”编辑:这篇稿件落到了当期的客座编辑手里。这位编辑名叫尼克劳斯·维尔特(Niklaus Wirth)11——没错,就是那个后来发明了 Pascal 语言的维尔特!维尔特觉得这个标题不够劲爆。为了“加快发表”,他决定把这篇论文改成一封“致编辑的信”,并且(重点来了)他自己动手改了标题12。
- “耸动的”标题:维尔特给它起了一个新标题:"Go To Statement Considered Harmful"(“GOTO 语句被认为有害”)13。
- 标题党的历史:更搞笑的是,"Considered Harmful"(“被认为有害”)这个短语在 1968 年已经是一个臭名昭著、用烂了的新闻界陈词滥调了14。它就像 1949 年《纽约时报》上的文章标题:“租金管制被认为有害”14。维尔特实质上是用了 1960 年代的“小编震惊体”。
学术界的“顶流骂战”
维尔特的标题党策略取得了空前的成功。这篇“信件”像一颗炸弹一样引爆了整个计算机科学界。它不再是一份“案例陈词”;它是一份“判决书”。
整个行业因此陷入了一场长达数十年的“圣战”。这场骂战是如此激烈,以至于它自己都成了一个梗:
- 1987 年(近 20 年后!): 程序员 Frank Rubin 在同一本杂志上发表了一篇(显然是憋了很久的)愤怒反驳,标题是:"'GOTO Considered Harmful' Considered Harmful"(“《GOTO 被认为有害》被认为有害”)9。
- 1987 年 5 月: 杂志显然很喜欢这种热度,于是刊登了更多关于那篇反驳信的回信,标题简直是官僚主义的杰作:"'“GOTO Considered Harmful” Considered Harmful' Considered Harmful?"(“《〈GOTO 被认为有害〉被认为有害》被认为有害?”)9。
- 迪杰斯特拉的“呵呵”: 面对这场由他(无意中)点燃的、持续了 20 年的熊熊大火,迪杰斯特拉本人终于做出了回应。他的回应标题堪称史上最冷漠、最致命的学术嘲讽:"On a somewhat disappointing correspondence"(“关于一场有点令人失望的通信”)11。
这简直是学术界的“我不是生气,我只是对你们所有人感到失望”。
教条的诞生:迅猛龙的袭击
维尔特的标题是如此成功,它创造了一种教条(Dogma)。一代又一代的程序员(包括我们中的许多人)被告知“GOTO 是魔鬼”,但我们中很少有人真正读过迪杰斯特拉的原文15。
这个现象被著名的 xkcd 漫画完美地捕捉了下来16。漫画里,一个程序员坐在电脑前,心想:“是重构这个程序的流程呢,还是用一个小小的‘GOTO’……唉,管他妈的‘良好实践’呢,能有多糟?”
他刚一敲下 goto,一只迅猛龙(Velociraptor)就破窗而入,把他扑倒在地16。
这就是“教条”:它是一种你并不理解其背后原理、但如果违反了就会有怪物(比如你的高级程序员同事,或者一只恐龙)来攻击你的“规则”。
迪杰斯特拉的真正论点:被忽视的“概念鸿沟”
好了,笑话到此为止。迪杰斯特拉的真正论点是什么?这才是你作为一名“复活”的程序员需要知道的核心。
他的论点远比“代码很乱”要深刻得多1。
迪杰斯特拉在他的原始论文(那篇标题温和的 "A Case Against...")中提出了一个关于人类认知局限的深刻见解17:
- 静态 vs 动态:我们的大脑(“智力”)非常擅长掌握“静态关系”。比如,阅读和理解你面前这页静止的代码文本18。
- “时间”的诅咒:但我们的大脑在“可视化随时间演进的过程”方面“相对发育不良”17。也就是说,在脑海中模拟程序在运行时的动态过程,对人类来说极其困难。
- “概念鸿沟”:一个好的程序,应该尽力“缩短静态程序(文本空间)和动态过程(时间空间)之间的概念鸿沟”17。
- GOTO 的原罪:而 GOTO 语句,正是那个彻底粉碎这种对应关系的元凶17。当你读到第 500 行代码时,你根本无法(仅凭这一行代码)知道程序是怎么到这里的。它是从第 42 行跳转过来的,还是从第 499 行顺序执行过来的,还是从第 8730 行跳转过来的?
迪杰斯特拉指出,GOTO 的存在,使得对程序进行“形式化验证”(Formal Verification)——即用数学方法证明一个程序是 100% 正确的——成为不可能1。
简而言之:迪杰斯特拉担心的不是你的代码是不是像“意大利面条”;他担心的是你的代码是不是一个无法被证明、无法被理解的、在逻辑上千疮百孔的“黑匣子”。
维尔特耸人听闻的标题把一场关于“数学可证明性”的深刻哲学思辨,变成了一场关于“代码风格”的街头斗殴。而历史证明,街头斗殴比哲学思辨更容易传播。
“解决方案”:用三块乐高积木重建巴别塔
所以,如果 GOTO(那座通向混乱的巴别塔)被禁止了,我们该用什么来重建它?
迪杰斯特拉的“有害”檄文只是摧毁了旧世界;而这场“结构化编程”运动(Structured Programming)需要一个坚实的理论基础来建设新世界19。
这个基础在 1966 年(比迪杰斯特拉的信还早两年)就已奠定,它就是大名鼎鼎的“结构化程序定理”(Structured Program Theorem),或者叫“伯姆-雅格皮尼定理”(Böhm-Jacopini theorem)1。
控制流的“三位一体”
这个定理用数学方法证明了一个惊人(在当时)的结论:任何可计算的函数,无论它有多复杂(哪怕它原来是用 GOTO 写成的意大利面条),都可以仅仅使用三种控制结构来重写20:
- 顺序(Sequence):先做 A,再做 B。
- 选择(Selection):如果(IF)条件为真,则做 A;否则(ELSE)做 B。
- 迭代(Iteration):当(WHILE)条件为真时,重复做 A。
就这三个。没了。
把理论变“香”:厨房和乐高
这个定理听起来就像你祖母的食谱一样枯燥。为了让你,这位技术时空旅行者,真正理解它的革命性,我们来用两个(不那么枯燥的)比喻。
-
比喻一:烹饪食谱21
想象一下用 GOTO 写的食谱:
1. 烧水。 2. GOTO 7。 3. 把面倒入水中。 4. GOTO 9。 5. 如果水开了 GOTO 3。... 7. 把盐放入水中。 8. GOTO 5。 9. 搅拌。
这简直是地狱。而“结构化食谱”(伯姆-雅格皮尼版)是这样的:
- (顺序):1. 烧水。 2. 放盐。
- (迭代):3. 直到(UNTIL)水沸腾:等待。
- (顺序):4. 把面倒入水中。
- (迭代):5. 当(WHILE)面条未熟时:搅拌。
- (选择):6. 如果(IF)你喜欢吃辣:加辣椒。
看,同样的程序,但逻辑是线性的、可读的、可预测的。
-
比喻二:乐高积木22
GOTO 就像是一种“传送门”乐高积木。你可以把程序的“入口”(比如一个循环的中间)连接到任何其他地方(比如另一个函数的内部)。你用这种积木搭出来的千年隼号,外表可能很酷,但内部结构一团糟,碰一下就会散架。而伯姆-雅格皮尼定理给了你三块标准积木23:
- 1x1 的砖块(顺序)
- 2x4 的分叉砖块(选择)
- 4x4 的平底板(迭代/循环)
定理证明了:用这三种标准积木,你依然可以搭出那艘一模一样的千年隼号。但这一次,它的结构是稳固的、模块化的、可理解的。
理论的“魔鬼细节”:那个隐藏的“但是……”
这听起来太完美了,对吧?我们用数学干掉了 GOTO,用三块积木统一了宇宙。
……然而,这里有一个巨大的、隐藏的“但是”。
这个定理证明了转换是可能的,但它没说转换后的代码是优雅的,甚至是可读的。
伯姆和雅格皮尼的原始证明(以及后来的许多转换算法)是……怎么说呢……相当的丑陋24。为了把一个复杂的 GOTO 迷宫强制转换成只有 WHILE 和 IF 的结构,那个转换算法经常需要引入一大堆额外的“辅助变量”和“状态标志位”20。
换句话说,你把“意大利面条式代码”(逻辑混乱)变成了“千层饼式代码”(一个巨大的 WHILE 循环,里面包着一个巨大的 SWITCH 语句,根据你刚才设置的哪个布尔标志位来决定下一步模拟哪个 GOTO 跳转)。
理论上,代码是“结构化”的。但实践中,它可能比原来的 GOTO 版本更难读懂,也几乎肯定更慢。
这场“结构化”革命的理论基础,居然是建立在这样一个“虽然可行,但代价感人”的证明之上。这简直是为我们故事中的下一位主角——那个胆敢质疑“先知”的“异端”——铺上了一条完美的红地毯。
“异端”唐纳德·克努特(Donald Knuth):编程界“最强王者”的异议
就在迪杰斯特拉的“GOTO 有害论”席卷学术界,即将成为不可动摇的教条时,另一个人站了出来。
这个人不是无名小卒。如果说迪杰斯特拉是编程界的“先知”和“哲学家”,那这位就是编程界的“最终 Boss”和“活着的圣人”。他就是唐纳德·克努特(Donald Knuth),那个(至今仍在)撰写多卷本圣经《计算机程序设计艺术》(The Art of Computer Programming)的人。
当克努特开口时,整个世界都会安静下来聆听。
1974 年,克努特发表了他对这场战争的回应。这篇论文的标题本身就是一种艺术,充满了克努特式的冷幽默和挑衅:"Structured Programming with go to Statements"(“使用 GOTO 语句的结构化编程”)1。
光是这个标题,就等于是在“GOTO 有害论”的教堂门口刻下了异端宣言。
克努特的传奇“开场白”
克努特深知自己是在挑战一个即将形成的“宗教”。因此,他用一种极其幽默的方式开始了这篇(极为严肃的)论文。他在引言里放了三句题记25:
“你走你的阳关道,我过我的独木桥。” —— 埃德娜·圣文森特·米莱(诗人)
“很可能,你走你的路,我走我的路。” —— 鲍勃·迪伦(歌手)
“你是否在忍受‘痛苦的消除’?” —— J. B. 威廉姆斯公司(一则泻药广告)26
这第三句,就是神来之笔。克努特在用一个双关语(Elimination,既指“消除 GOTO”,也指“排泄”)疯狂暗示:你们这帮“GOTO 消除原教旨主义者”,是不是便秘了?
克努特的论点:“平衡”与“实用”
克努特在论文开头就表明了立场:他不是来“平反”GOTO 的26。他写道,他知道这篇文章会惹恼两类人:一类是“GOTO 死忠粉”(以为克努特要带他们回到随地大小便的好日子),另一类是“GOTO 激进分子”(“克努特这种老顽固怎么还不滚蛋?”)。
他真正的目的,是寻求一种“关于 GOTO 语句适当角色的、合理的、平衡的观点”26。
他完全同意迪杰斯特拉的观点:无限制、无纪律的 GOTO 是有害的。但他认为,在某些特定情况下,一个有纪律、易于理解的 GOTO,恰恰是最清晰、最高效、最优雅的解决方案26。
克努特的“GOTO 合法用例”
他举了几个经典的例子,其中最著名的一个,至今仍是 goto 的“合法辩护词”:
-
合法用例一:跳出“多层嵌套循环”27
想象一下这个场景:你在一个for循环里,又嵌套了另一个for循环,目的是在一个二维数组中搜索一个特定的值。// 目标:在一个 100x100 的矩阵中找到第一个 0 for (int i = 0; i < 100; i++) { for (int j = 0; j < 100; j++) { if (matrix[i][j] == 0) { // 找到了!现在怎么办? // break; <-- 这只能跳出内层循环 (j 循环) } } // i 循环会在这里继续... 这不是我们想要的! }你该怎么跳出两层循环?
-
“纯洁的”结构化方案:你必须引入一个辅助的布尔变量,比如
bool found = false;28。bool found = false; for (int i = 0; i < 100; i++) { for (int j = 0; j < 100; j++) { if (matrix[i][j] == 0) { found = true; break; // 跳出 j 循环 } } if (found) { break; // 跳出 i 循环 } }这种方案是“纯洁的”,但它更臃肿,引入了额外的状态变量,而且(在某些情况下)效率更低28。
-
克努特的“异端”方案:
for (int i = 0; i < 100; i++) { for (int j = 0; j < 100; j++) { if (matrix[i][j] == 0) { goto found_it; // 一步到位 } } } found_it: // 在这里继续...克努特认为,这个
goto更清晰、更易读、更高效28。它完美地表达了程序员的意图(“我受够了,马上离开这里”),而没有被“单一出口”的循环教条所束缚。
-
-
合法用例二:著名的“过早优化”
这篇论文还诞生了另一句被(同样被严重误读的)名言。克努特在讨论代码效率时写道:“我们应该忘记那些微小的效率提升,大概 97% 的时间里都是如此:过早的优化是万恶之源(Premature optimization is the root of all evil)。”29大多数人只记住了后半句,并把它当作“永远不要优化”的借口。但克努特的重点是“97%”。他的意思是,在你代码中那关键的 3%(比如一个被调用十亿次的内层循环),效率就是一切29。
他用了一个绝妙的比喻:一个程序员如果(为了纯洁性)在生产代码里保留了昂贵的检查,就像“一个水手在陆地上训练时穿着救生衣,但真要出海时却把它留在了岸上。”30。但他紧接着说:“然而,如果救生衣极其昂贵,而且他是个游泳健将……那么这个水手也并非那么愚蠢。”30
他的意思是:不要盲目地牺牲效率去迎合“结构化”的教条。在关键时刻,如果一个
goto能让你的代码快 15%,那就去用它!
战争的终结:用“先知”反驳“教条”
最后,克努特给出了致命一击。他指出,迪杰斯特拉本人,那个“先知”,已经对这场由他引发的“宗教战争”感到不适了。克努特在论文的结尾引用了迪杰斯特拉自己后来的一段话31:
“请不要掉入这个陷阱,认为我对 [goto 语句] 极端教条。我有一种不安的感觉,其他人正在把它变成一种宗教,就好像编程的根本问题可以通过一个简单的技巧、一种简单的编码纪律来解决一样!”31
这是一次完美的“绝杀”。克努特等于是在说:“嘿,伙计们,别吵了。我刚和你们的‘教皇’聊过,连他都觉得你们这帮狂热分子太离谱了。”
这场“GOTO 战争”的核心,从来都不是“GOTO vs 非 GOTO”。它是一场更深层次的、贯穿整个编程史的代理人战争:理论纯洁性(迪杰斯特拉) vs 开发者实用主义(克努特)。
迪杰斯特拉试图让代码可以被机器(数学)所证明。
克努特则试图让代码可以被人类所理解和使用。
而历史的最终判决是……
“真正的结局”:GOTO永生,只是被“驯化”了
你现在可能在想:“好吧,这帮学者吵了几十年,最后到底谁赢了?”
答案很简单:克努特赢了1。
迪杰斯特拉赢得了课堂——他让“结构化编程”成为了计算机科学专业的必修课。
但克努特赢得了战场——他让“实用主义”成为了编写操作系统的核心准则。
GOTO 在 C 语言中的“光荣复活”
在你沉睡的这段时间里,一种名为 C 语言的新语言(我们将在下一章详细介绍)出现了,它迅速成为了系统编程的王者。C 语言是一种“结构化”语言,它有 while、for 和 if/else。
但是……它也保留了 goto 关键字。
为什么?因为 C 语言的作者们(丹尼斯·里奇和肯·汤普逊)和克努特一样,是实用主义者。他们知道在编写操作系统这种复杂、高性能、高风险的代码时,你需要“安全阀”。
在现代 C 语言中,goto 至今仍然是一个被广泛接受的、常见的、甚至是被推荐的惯用法1。但它几乎只被用于一个(也是克努特所倡导的)特定目的:错误处理和资源清理1。
终极范例:Linux 内核与 SQLite 中的 goto cleanup
看看那些地球上质量最高、被审查最严格的 C 代码——比如 Linux 内核,或者 SQLite(那个可能正运行在你手机或浏览器里的数据库)。它们的代码里充满了 goto32。
但它们不是“意大利面条”。它们是一种极其优雅和清晰的模式,通常被称为 "goto cleanup"32:
想象一个 C 函数,它需要:
- 分配一块内存。
- 打开一个文件。
- 对文件执行操作。
- 如果任何一步失败,必须按相反的顺序释放所有已获取的资源,然后退出。
-
“纯洁的”结构化方案(地狱般的“箭头代码”):11
int function() { if (allocate_memory() == SUCCESS) { if (open_file() == SUCCESS) { if (do_operation() == SUCCESS) { // 成功! close_file(); free_memory(); return 0; } else { // 操作失败 close_file(); free_memory(); return -1; } } else { // 打开文件失败 free_memory(); return -1; } } else { // 分配内存失败 return -1; } }这种代码被称为“箭头代码”(Arrow Code),因为它向右缩进得越来越深,形成一个箭头。它极难阅读,而且清理逻辑在每个失败分支里都重复了一遍,极易出错。
-
克努特的“实用”方案(Linux 内核风格):32
int function() { int err = 0; char *buffer = malloc(1024); if (buffer == NULL) { err = -ENOMEM; goto out; // 失败 -> 跳转到出口 } FILE *f = fopen("file.txt", "r"); if (f == NULL) { err = -EIO; goto out_free_buffer; // 失败 -> 跳转到清理 buffer } if (do_operation(f) != SUCCESS) { err = -EFAULT; goto out_close_file; // 失败 -> 跳转到清理 file } // 成功路径的清理(按顺序) out_close_file: fclose(f); out_free_buffer: free(buffer); out: return err; }看到这个“瀑布式”的清理结构了吗33?所有失败的
goto都只向前跳转到相应的清理标签。所有的清理代码都只存在一次。这是 C 语言所能达到的最干净、最健壮、最不易出错的资源管理模式34。
这不是迪杰斯特拉所憎恶的“意大利面条”。这是克努特所倡导的“有纪律的、清晰的、作为最后手段”的 goto。
GOTO 的“孩子们”:一张翻译手册
所以,先知(迪杰斯特拉)是对的:无限制的 GOTO 是有害的。异端(克努特)也是对的:有纪律的跳转是必要的。
最终的结局是什么?
GOTO 从未死去。它只是被“驯化”了1。
“结构化编程”革命的真正遗产,不是消灭了跳转,而是专门化了跳转。我们拿走了那个万能的、危险的 GOTO,把它分解成了一套“安全”的、有特定用途的“有限 GOTO”35。
对于你这位“复活”的程序员,这里是你的现代控制流“翻译手册”:
| 你的 BASIC 命令 | 现代“驯化”的等价物 | 它的“项圈”(限制) |
|---|---|---|
GOTO 500 (跳出循环) |
break; |
高度驯化35。只能在循环或 switch 内部使用。只能向前跳转到紧跟在循环之后的那一行代码36。 |
GOTO 100 (跳回循环开始) |
continue; |
高度驯化36。只能在循环内部使用。只能向前(或向上)跳转到下一次循环迭代的开始处36。 |
GOSUB 1000... RETURN |
myFunction(); |
完全驯化。这是一个“有往有回”的 GOTO。它跳转,但它承诺会通过 return 跳回到它离开的地方。 |
GOTO 9999 (跳到函数末尾) |
return value; |
驯化37。一个“提前退出”的 GOTO。它只能跳转到当前函数的末尾,并把控制权交还给调用者36。 |
ON ERROR GOTO... |
try {... } catch {... } |
GOTO 的“豪华版”38。这是最强大的 GOTO!它不仅能跳转,还能跨函数、向上跳转,一路“解开调用堆栈”,直到找到一个 catch 捕获器37。 |
GOTO cleanup_label |
goto cleanup_label; (在 C/C++ 中) |
克努特的“野性尚存版”1。它依然存在,但社会(编程规范)只允许它在极少数情况下使用,比如上一节的“资源清理”模式。 |
章节总结:欢迎回家,实用主义者
所以,你看。你并不是“恐龙”,你的知识也没有“过时”。你只是拥有那个“万能工具”的原始蓝图。
你所熟知的 GOTO 并没有死;它只是进化了。它成为了一个“共同祖先”,它的 DNA 分裂并演化成了现代语言中五六个更安全、更专业的后代。
迪杰斯特拉发起的“圣战”是必要的,因为它迫使我们停止了随心所欲的跳转,开始思考“可维护性”和“可证明性”。而克努特的“异端”反驳也是必要的,因为它把我们从“纯洁性”的教条中拯救出来,带回了“实用主义”的现实世界。
你,作为一名掌握着“GOTO 简史”的技术时空旅行者,现在比那些只知道“GOTO 是有害的”却不知道为什么的年轻程序员,拥有了更深刻的理解。
你理解了:编程的本质,就是在“纯洁的理论”和“混乱的现实”之间,找到那个(由克努特捍卫的)脆弱而美丽的平衡点。
现在,让我们离开这场哲学辩论,走进下一章。我们将进入一个充满危险和机遇的新世界——C 语言的“Bug 谷”和“变量海”。在那里,你将发现,GOTO 只是你将要面对的“怪物”中,最微不足道的那一个。
参考链接
-
程序员复活手册:从BASIC到AI(大纲) ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
Line number - Wikipedia, 访问时间为 十月 27, 2025 ↩︎
-
A historical question: why does BASIC have line numbers in the first place? : r/c64 - Reddit, 访问时间为 十月 27, 2025 ↩︎
-
language design - Why did BASIC use line numbers? - Software ..., 访问时间为 十月 27, 2025 ↩︎
-
Spaghetti code Memes - ProgrammerHumor.io, 访问时间为 十月 27, 2025 ↩︎
-
TerrariaClone - An incomprehensible hellscape of spaghetti code : r ..., 访问时间为 十月 27, 2025 ↩︎
-
Ask Reddit: Looking for your best examples of spaghetti code : r ..., 访问时间为 十月 27, 2025 ↩︎ ↩︎
-
Can you give examples of spaghetti code and non-spaghetti code? - Reddit, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎
-
Go to Statement Considered Harmful (1968) [pdf] - Hacker News, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎
-
Edgar Dijkstra: Go To Statement Considered Harmful - CWI, 访问时间为 十月 27, 2025 ↩︎
-
I'd Consider That Harmful, Too - Coding Horror, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎
-
Code Reads #2: Dijkstra's “Go To Statement Considered Harmful” - Wordyard, 访问时间为 十月 27, 2025 ↩︎
-
Wikipedia - Considered harmful, 访问时间为 十月 27, 2025 ↩︎
-
Considered harmful - Wikipedia, 访问时间为 十月 27, 2025 ↩︎ ↩︎
-
The Goto, Spaghetti and the Velociraptor - I Programmer, 访问时间为 十月 27, 2025 ↩︎
-
292: goto - explain xkcd, 访问时间为 十月 27, 2025 ↩︎ ↩︎
-
E.W. Dijkstra Archive: A Case against the GO TO Statement. (EWD215), 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎
-
Statement Considered Harmful - Communications of the ACM, 访问时间为 十月 27, 2025 ↩︎
-
Structured programming - Wikipedia, 访问时间为 十月 27, 2025 ↩︎
-
Structured program theorem - Wikipedia, 访问时间为 十月 27, 2025 ↩︎ ↩︎
-
Hard Coding Concepts Explained with Simple Real-life Analogies, 访问时间为 十月 27, 2025 ↩︎
-
What are your favorite analogies to explain programming? - DEV Community, 访问时间为 十月 27, 2025 ↩︎
-
Concept:Böhm-Jacopini theorem - 101wiki, 访问时间为 十月 27, 2025 ↩︎
-
How to prove the structured program theorem? - Computer Science Stack Exchange, 访问时间为 十月 27, 2025 ↩︎
-
Knuth on go to – 1974 » Steve on Image Processing with MATLAB - MathWorks Blogs, 访问时间为 十月 27, 2025 ↩︎
-
Structured Programming with go to Statements - W. Richard Stevens, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎
-
r/ProgrammerHumor - xkcd: goto - Reddit, 访问时间为 十月 27, 2025 ↩︎
-
c++ - How to use goto to break a nested loop - Stack Overflow, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎
-
Programming Myths and Folklore: The Origin of "Premature optimization is the root of all evil", 访问时间为 十月 27, 2025 ↩︎ ↩︎
-
Structured Programming with go to Statements DONALD E. KNUTH INTRODUCTION, 访问时间为 十月 27, 2025 ↩︎ ↩︎
-
language agnostic - GOTO still considered harmful? - Stack Overflow, 访问时间为 十月 27, 2025 ↩︎ ↩︎
-
Error handling via GOTO in C - Ayende @ Rahien, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎
-
Valid use of goto for error management in C? - Stack Overflow, 访问时间为 十月 27, 2025 ↩︎
-
Any good idioms for error handling in straight C programs? - Stack Overflow, 访问时间为 十月 27, 2025 ↩︎
-
break vs goto: Which is more appropriate in the following bool ..., 访问时间为 十月 27, 2025 ↩︎ ↩︎
-
Jump statements - break, continue, return, and goto - C# reference ..., 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎
-
[Belay the C++] Exceptions are just fancy gotos : r/cpp - Reddit, 访问时间为 十月 27, 2025 ↩︎ ↩︎
-
try/catch vs. goto - CodeGuru Forums, 访问时间为 十月 27, 2025 ↩︎