IT 论文精读 · PAPER 37
Edsger W. Dijkstra · Communications of the ACM · 1968
1968 年,荷兰计算机科学家 Edsger Dijkstra 给一本学术期刊写了一封两页的短信,标题是《GOTO 语句有害》。信里没有算法、没有公式,却成了软件史上最有名的文章之一,直接改写了「代码该怎么写」这件事。今天你写的 if / while / for、以及几乎每种语言都劝你少用甚至干脆删掉的 goto——源头就是这封信。
早年的程序像一张写满编号行的清单,goto 是一条命令:「别往下读了,直接跳到第 87 行接着执行」。它能往前跳、往后跳、跳进跳出任何地方。方便是真方便,但一个稍大的程序里塞满这种跳转,读起来就像一团缠死的耳机线——业内管这叫「面条代码」。
他点破了一件很微妙的事:我们读代码是从上往下、一行行看的(静态的文字),可程序真正跑起来是在时间里一步步走的(动态的过程)。写程序的人脑子里得始终对上这两件事——「我读到的这一行,对应程序跑到哪一步」。而他有个关于人的观察:人天生擅长看懂静态的排列,却很不擅长在脑中想象一个随时间演化的过程。所以好代码的关键,是让静态的文字尽量像动态执行的一张地图。
goto 恰恰把这张地图撕烂了。打个比方:读一本普通的书,一个书签就能标记「我读到哪了」;可要是这本书每一页都写着「现在跳去翻第几页」、跳来跳去,那个书签就没意义了——你光看书签,根本说不清故事进行到哪、之前发生过什么。
Dijkstra 的主张是:只用三种「不乱跳」的积木——顺着往下(顺序)、二选一(if 分支)、重复做(循环)——就足以写出任何程序;而这样写出来的代码,永远能用「读到第几行 +(如果在循环里)转了几圈」这样一个清清楚楚的书签,标出程序此刻走到哪。文字重新变回了可靠的地图。
这封信点燃了「结构化编程」运动。此后新语言纷纷把 if/while/for 做成一等公民、把 goto 打入冷宫;「代码要让人读得懂」第一次被摆到和「能跑」同等重要的位置。今天你几乎不会在正经代码里见到 goto,多半就拜这封信所赐。诚实说一句:后来 Knuth 等人指出,少数情形(比如出错时一跳跳出好几层循环)用一次跳转反而更清楚——所以主流做法是「极力少用」,而非「绝对禁止」。
代码是静态的文字,执行是动态的过程;人脑擅长前者、拙于后者。goto 能任意乱跳,让「读到哪」对不上「跑到哪」,文字不再是执行的地图。改用顺序 / 分支 / 循环三种积木,就永远能用一个清楚的书签标出程序此刻的位置——代码于是可读、可查、可信。
想看「进度坐标」的精确论证、控制流对比图与后续争论? → 切到精读版
Dijkstra 在这封两页的信里论证:goto(无限制跳转)会让程序的静态文字与它执行时的动态过程严重脱节,使人无法用一组简洁的「坐标」说清「程序此刻走到哪、之前经历了什么」;而只要把控制流限制在顺序、选择(if)、循环三种结构里,这组坐标就总是存在——代码于是可读、可推理、可验证。这封信点燃了结构化编程(structured programming)运动,奠定了现代控制结构。
if-then-else,选择)、重复执行一段(while/for,循环 / 迭代)。作者 Edsger W. Dijkstra(荷兰,1972 年图灵奖得主),文章 1968 年 3 月发表于《Communications of the ACM》,其实是一封「致编辑的信」。Dijkstra 原拟标题《反对 GOTO 语句的理由》,被编辑 Niklaus Wirth 改成更醒目的《Go To Statement Considered Harmful》。它上承 Böhm 与 Jacopini 1966 年的理论结果,下启 1970 年代的结构化编程运动,以及 Dijkstra、Hoare、Dahl 合著的《Structured Programming》(1972)。
1960 年代的主流语言(FORTRAN、汇编,乃至 ALGOL)都以 goto 为核心控制手段:程序是一串带编号的语句,靠 goto 在其间任意跳转。这在小程序里没问题,可程序一大,跳转交织成网,读者几乎无法在脑中重建「执行到底怎么走」。
Dijkstra 想追问一个更根本的问题:我们凭什么能相信一个程序是对的?他给出一个关于人的观察:人的智力更擅长把握静态的结构关系,而很不擅长在脑中想象随时间演化的过程。而程序恰恰有两副面孔——写在纸上的是静态文本,跑起来是动态过程。写程序、读程序、调程序,本质都是在这两者之间来回对应。若二者的对应简单清晰,我们就抓得住;若混乱,我们就失控。goto 正是让二者失控的元凶。
Dijkstra 的核心论证不是「goto 丑」,而是一件很精确的事:能不能给「程序此刻的进度」找到一组简洁、有意义的坐标。他逐一检查了结构化的控制流:
关键在于:这几种结构化控制流,都能让我们从「控制走到哪」系统地推出一组「进度坐标」——而这组坐标,正是我们谈论程序状态、写下「执行到这里时 X 必成立」这类不变式的立足点。没有它,正确性推理就无从下脚。
而 goto 把这一切打碎:一旦允许任意跳转,某个文本位置可以从四面八方被抵达,「读到第几行」不再蕴含「之前经历了什么」,那个文本指针退化成一个无意义的坐标。你再也没法简洁地刻画进度,也就没法在脑中或纸上稳稳地推理这段程序。Dijkstra 由此断言:goto「太原始了,原始到成了把程序搅乱的诱惑」。
Böhm 与 Jacopini 早在 1966 年就证明:任何可计算的流程,都能只用顺序、选择、循环三种结构表达,goto 在能力上并非必需。Dijkstra 的主张顺理成章:主动放弃 goto,把程序限制在这三种「进度坐标始终存在」的积木里。代价是有时要多写一个布尔变量、多绕一点,但换来的是「文本 ≈ 执行地图」这个可贵性质。要注意,他反对的是无限制的 goto,而非一切跳转——结构化的分支与循环,本质上就是「被驯服的跳转」。
这是一篇立论文章,没有实验,它的「结果」是论证本身的说服力,加上它援引的理论支撑:Böhm–Jacopini 定理保证了「删掉 goto 不损失任何表达能力」,让「少用 / 不用 goto」从审美偏好变成有依据的工程主张。文章给出的可操作结论极简却极硬——用顺序 / 选择 / 循环组织一切控制流,并留下那句流传至今的判断:「程序员的水平,是其代码中 goto 密度的减函数」。它的影响力不靠数字,而靠此后二十年整个行业的实践为它背书。
break / continue / 多重 return / 异常,其实都是「受约束的跳转」,用滥了同样能把控制流搞乱——问题从来不在 goto 这个关键字本身。① 一句话:goto 让静态代码文本与动态执行过程脱节,改用顺序 / 选择 / 循环即可修复——结构化编程的宣言。
② 洞见:人脑擅长静态结构、拙于想象动态过程;好代码要让文本成为执行的可靠地图。
③ 核心机制:结构化控制流总能给「进度」一组简洁坐标(文本指针 +(循环)计数 +(调用)栈);这正是写不变式、做正确性推理的立足点。
④ goto 之害:任意跳转让文本位置不再蕴含「之前经历了什么」,坐标退化,推理失据。
⑤ 理论支撑:Böhm–Jacopini(1966)证明顺序 / 选择 / 循环即足以表达一切流程,删 goto 不损表达力。
⑥ 名句:「程序员水平是其代码 goto 密度的减函数」;他反对的是「无限制」跳转,而非一切跳转。
⑦ 影响:引爆结构化编程,if/while/for 成语言标配,可读性 / 可证明性升为一等指标。
⑧ 局限:Knuth 指出个别情形 goto 更优(「极力少用」而非全禁);goto 只是无结构控制流的替罪羊;现代错误处理仍有正当 goto。