引言
在 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 (面条式代码):程序执行流程在一个函数中毫无规律地跳来跳去。
例如:
gotoLabelA; ... LabelA: ... gotoLabelC; ... LabelC: ... gotoLabelB;当程序可以跳转到几乎任何位置时,代码就会变得非常难以理解,也很难验证它是否正确。
现代软件工程一直反对这种写法,因为它会降低代码的可读性、可测试性以及可维护性。
真正的问题是 无结构的跳转 ,而不是 goto 这个关键字本身。
为什么 Linux Kernel 推荐使用 goto
Linux Kernel 官方文档专门讨论过这个问题。
它并没有禁止 goto ,相反,它建议将 goto 用于 集中式资源清理(centralized cleanup)和错误处理(error handling) 。
Coding Style 中指出,当一个函数有多个退出点,并且这些退出点都需要执行相同的清理工作时, goto 会非常有用。( Linux Kernel Documentation )
与其在每一个错误判断后重复写清理代码,不如统一跳转到一个清理标签。
例如:
char *buffer = kmalloc(...); if(!buffer) return-ENOMEM; if(condition) gotoout_free_buffer; ... out_free_buffer: kfree(buffer); returnret;这种写法有几个明显的优点:
在 C 中,资源管理并不容易
goto 之所以至今仍然有价值,最主要的原因是 C 本身没有自动资源管理机制。
一个函数可能需要申请多种资源,例如:
假设一个函数按顺序申请了这些资源:
Allocate A Allocate B Allocate C Allocate D如果申请 D 时失败,那么程序必须按照相反的顺序释放资源:
Free C Free B Free A如果没有统一的清理机制,那么每一条错误处理路径都不得不重复写几乎相同的资源释放代码。
随着函数越来越大,这种代码很快就会变得难以维护。
CERT C 也推荐这种模式
CERT C Secure Coding Standard 得出了相同的结论。
它的 MEM12-C 指南建议:当函数在出错时需要释放多个资源,可以使用 goto chain 来完成统一清理。( CMU SEI )
与其在每一个错误处理分支中重复编写清理代码,不如把清理逻辑组织成一组标签,每个标签负责释放一层已经申请的资源,并按照相反的顺序逐步完成清理,最后退出函数。
这种模式有几个优点:
需要注意的是,CERT 强调,这种建议仅适用于 单个函数内部的局部错误处理 ,并不是鼓励在整个程序中随意使用 goto 。
Linux Kernel 中的真实代码
这并不仅仅是理论上的建议。
Linux Kernel 本身就有大量函数采用了多级清理标签。
历史上, copy_process 一直都是这种设计模式最经典的例子之一。虽然不同版本的 Linux Kernel 中,清理标签的名称已经发生了一些变化,但在今天的 kernel/fork.c 以及大量驱动代码中,仍然可以看到大量采用向前跳转(forward goto )的清理链。( SEI Wiki )
典型的标签包括:
gotobad_fork_cleanup_mm; gotobad_fork_cleanup_fs; gotobad_fork_cleanup_io;每一个标签只负责释放在当前阶段之前已经成功申请的资源。
虽然这个函数包含了很多 goto 语句,但整个控制流仍然是有结构的,因为所有跳转都是 向前 进入统一的清理区域,然后退出函数。
为什么现代 C++ 很少需要 goto
C++ 通过 RAII(Resource Acquisition Is Initialization) 基本解决了这个问题。
对象离开作用域时,会自动释放自己所持有的资源。
例如:
std::unique_ptr std::vector std::string std::lock_guard因此,大多数情况下都不再需要专门的清理标签。
即使发生异常(exception),对象的析构函数也会自动执行。
因此,在现代 C++ 中,很少再需要使用 goto 来完成资源管理。
语言本身已经提供了更加安全的解决方案。
什么时候应该使用 goto ?
合理的使用场景包括:
不合理的使用场景包括:
有一个简单的判断原则:
如果所有 goto 都是为了跳转到同一个清理区域,那么这种写法通常是可以接受的。
如果程序执行流程在函数中到处跳来跳去,那么很可能就是代码设计出了问题。
总结
goto 本身既不是好的,也不是坏的。
1968 年那篇著名论文批评的是混乱、无结构的程序,而不是规范的错误处理方式。
如今,Linux Kernel 作为世界上最大的 C 项目之一,仍然大量使用 goto ,因为它解决了一个非常现实的工程问题:在没有自动资源管理机制的语言中,实现统一的资源清理。( Linux Kernel Documentation )
现代 C++ 已经通过 RAII、智能指针以及确定性的析构机制,在很大程度上取代了这种模式。
但对于现代 C 来说,尤其是在系统编程、嵌入式开发以及操作系统内核中, goto 仍然是一个重要且实用的工具。
和任何语言特性一样,它真正的价值,并不取决于关键字本身,而在于你为什么使用它,以及如何使用它。
引用链接
Linux Kernel Documentation: https://www.kernel.org/doc/html/latest/process/coding-style.html "Linux kernel coding style — The Linux Kernel documentation"
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"
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"
Linux Kernel Documentation: https://www.kernel.org/doc/html/latest/process/coding-style.html "Linux kernel coding style — The Linux Kernel documentation"