引言
在 C 语言中,很少有哪个关键字像 goto 一样引发如此多的争议。
几十年来,程序员一直被告知要避免使用它。很多教材甚至直接总结为一句话:
“永远不要使用 goto。”
这种观点很大程度上源于 Edsger W. Dijkstra 于 1968 年发表的著名论文 《Go To Statement Considered Harmful》。
然而,如果你去阅读 Linux Kernel 的源代码——这个世界上规模最大、最成功的软件项目之一——你很快就会发现一个令人意外的事实:
goto 几乎随处可见。
这是否意味着 Linux Kernel 无视现代软件开发实践?
当然不是。
真实情况远比这个复杂。
真正的问题从来都不是 goto 本身
Dijkstra 并没有认为所有 goto 的使用都是错误的。
他批评的是无结构的控制流(unstructured control flow),也就是人们常说的 spaghetti code(面条式代码):程序执行流程在一个函数中毫无规律地跳来跳去。
例如:
goto LabelA;...LabelA:...goto LabelC;...LabelC:...goto LabelB;
当程序可以跳转到几乎任何位置时,代码就会变得非常难以理解,也很难验证它是否正确。
现代软件工程一直反对这种写法,因为它会降低代码的可读性、可测试性以及可维护性。
真正的问题是无结构的跳转,而不是 goto 这个关键字本身。
为什么 Linux Kernel 推荐使用 goto
Linux Kernel 官方文档专门讨论过这个问题。
它并没有禁止 goto,相反,它建议将 goto 用于集中式资源清理(centralized cleanup)和错误处理(error handling)。
Coding Style 中指出,当一个函数有多个退出点,并且这些退出点都需要执行相同的清理工作时,goto 会非常有用。(Linux Kernel Documentation [1])
与其在每一个错误判断后重复写清理代码,不如统一跳转到一个清理标签。
例如:
char *buffer = kmalloc(...);if (!buffer)return -ENOMEM;if (condition)goto out_free_buffer;...out_free_buffer: kfree(buffer);return ret;
这种写法有几个明显的优点:
减少重复的清理代码
保持所有错误处理路径一致
后续修改代码时更加安全
减少不必要的嵌套
更容易维护
在 C 中,资源管理并不容易
goto 之所以至今仍然有价值,最主要的原因是 C 本身没有自动资源管理机制。
一个函数可能需要申请多种资源,例如:
memory
files
sockets
mutexes
locks
假设一个函数按顺序申请了这些资源:
Allocate AAllocate BAllocate CAllocate D
如果申请 D 时失败,那么程序必须按照相反的顺序释放资源:
Free CFree BFree A
如果没有统一的清理机制,那么每一条错误处理路径都不得不重复写几乎相同的资源释放代码。
随着函数越来越大,这种代码很快就会变得难以维护。
CERT C 也推荐这种模式
CERT C Secure Coding Standard 得出了相同的结论。
它的 MEM12-C 指南建议:当函数在出错时需要释放多个资源,可以使用 goto chain 来完成统一清理。(CMU SEI [2])
与其在每一个错误处理分支中重复编写清理代码,不如把清理逻辑组织成一组标签,每个标签负责释放一层已经申请的资源,并按照相反的顺序逐步完成清理,最后退出函数。
这种模式有几个优点:
资源始终按照正确的顺序释放
清理代码只保留一份
后续增加新的资源更加容易
维护成本明显更低
需要注意的是,CERT 强调,这种建议仅适用于单个函数内部的局部错误处理,并不是鼓励在整个程序中随意使用 goto。
Linux Kernel 中的真实代码
这并不仅仅是理论上的建议。
Linux Kernel 本身就有大量函数采用了多级清理标签。
历史上,copy_process() 一直都是这种设计模式最经典的例子之一。虽然不同版本的 Linux Kernel 中,清理标签的名称已经发生了一些变化,但在今天的 kernel/fork.c 以及大量驱动代码中,仍然可以看到大量采用向前跳转(forward goto)的清理链。(SEI Wiki [3])
典型的标签包括:
goto bad_fork_cleanup_mm;goto bad_fork_cleanup_fs;goto bad_fork_cleanup_io;
每一个标签只负责释放在当前阶段之前已经成功申请的资源。
虽然这个函数包含了很多 goto 语句,但整个控制流仍然是有结构的,因为所有跳转都是向前进入统一的清理区域,然后退出函数。
为什么现代 C++ 很少需要 goto
C++ 通过 RAII(Resource Acquisition Is Initialization) 基本解决了这个问题。
对象离开作用域时,会自动释放自己所持有的资源。
例如:
std::unique_ptrstd::vectorstd::stringstd::lock_guard
因此,大多数情况下都不再需要专门的清理标签。
即使发生异常(exception),对象的析构函数也会自动执行。
因此,在现代 C++ 中,很少再需要使用 goto 来完成资源管理。
语言本身已经提供了更加安全的解决方案。
什么时候应该使用 goto?
合理的使用场景包括:
集中式资源清理
释放资源
错误处理
从多层初始化逻辑中快速退出
不合理的使用场景包括:
用来实现循环
替代 if
在大型函数中向后跳转
写出 spaghetti code
在互不相关的代码之间随意跳转
有一个简单的判断原则:
如果所有 goto 都是为了跳转到同一个清理区域,那么这种写法通常是可以接受的。
如果程序执行流程在函数中到处跳来跳去,那么很可能就是代码设计出了问题。
总结
goto 本身既不是好的,也不是坏的。
1968 年那篇著名论文批评的是混乱、无结构的程序,而不是规范的错误处理方式。
如今,Linux Kernel 作为世界上最大的 C 项目之一,仍然大量使用 goto,因为它解决了一个非常现实的工程问题:在没有自动资源管理机制的语言中,实现统一的资源清理。(Linux Kernel Documentation [4])
现代 C++ 已经通过 RAII、智能指针以及确定性的析构机制,在很大程度上取代了这种模式。
但对于现代 C 来说,尤其是在系统编程、嵌入式开发以及操作系统内核中,goto 仍然是一个重要且实用的工具。
和任何语言特性一样,它真正的价值,并不取决于关键字本身,而在于你为什么使用它,以及如何使用它。
引用链接
[1] Linux Kernel Documentation: https://www.kernel.org/doc/html/latest/process/coding-style.html "Linux kernel coding style — The Linux Kernel documentation"[2] CMU SEI: https://cmu-sei.github.io/secure-coding-standards/sei-cert-c-coding-standard/recommendations/memory-management-mem/mem12-c/ "MEM12-C. Consider using a goto chain when leaving a function on error when using and releasing resources | CERT Secure Coding"[3] SEI Wiki: https://wiki.sei.cmu.edu/confluence/x/odYxBQ "MEM12-C. Consider using a goto chain when leaving a function on error when using and releasing resources - SEI CERT C Coding Standard - Confluence"[4] Linux Kernel Documentation: https://www.kernel.org/doc/html/latest/process/coding-style.html "Linux kernel coding style — The Linux Kernel documentation"