Zig語(yǔ)言終結(jié)Goto?不容小覷的新方案
AI生成的图片
介绍啊,goto
,编程中最具争议的四个字符。自编程诞生以来,它就一直存在,并且一直是辩论和争议的焦点。“跳跃”到指令集中的不同指令对计算机来说是一个自然的操作(实际上,在机器级代码中,跳转或goto
指令相当常见)。因此,在高级编程语言中使用goto
看起来也很自然。然而,你不需要我告诉你,goto
在编程社区中名声不佳。goto
已经成为几十年来争议的目标。编程界的大腕,如迪杰斯特拉(没错,就是Dijkstra算法的迪杰斯特拉),公开批评这个关键字。
尽管名声不太好,仍有一群硬核程序员坚信goto
的强大和实用性。其中就包括Linux内核开发者。多年来,goto
一直是Linux内核代码库中的一个常用元素,在代码库中出现了超过200,000次。
注意:我通过使用
ripgrep
在 Linux 内核代码中搜索goto
,找到了大约 200,000 个goto
。这个数字只是一个估算,可能并不完全准确。
为什么这些开发者选择忽视像迪杰斯特拉(Dijkstra)这样的大师几十年来的忠告?
在这篇文章里,我想探讨下goto
在Linux内核中的使用,以及Zig(大家喜欢的新兴系统编程语言)如何为Linux内核中常见的goto
使用模式提供更优雅的解决方案。
goto
的简述历史
如果你从未写过一行BASIC或FORTRAN代码,goto
是一个关键字,它允许你无视程序结构跳转到任何地方。听起来很强大,因为它确实很强大。goto
让你可以编写出比其他任何编程工具更复杂和高效的代码。在编程的早期,goto
是一个救命稻草,很多人认为它的存在是不可避免的。
一个在TI-84计算器上用TI-BASIC编写的简单程序,使用了goto语句。在这个例子中,程序开头设定了一个标签。如果用户输入的不是一个正数,程序会跳转到goto语句,程序会从该标签开始执行。
FORTRAN,作为第一种高级编程语言,在1956年引入了goto
语句(参见这里)。然而,几乎在它被引入之后不久,编程社区就开始意识到goto
的潜在危险。1959年,计算机科学家海因茨·策曼尼克曾批评使用goto
,甚至表示在高级编程语言中这种功能是多余的。对goto
最严厉的批评之一来自于艾兹格·戴克斯特拉在1968年发表的著名文章《goto
语句被认为是有害的》(Goto Statement Considered Harmful)。在这篇文章中,他认为,能够破坏程序时间流程的工具本质上是有害的,应当避免使用。他指出,在程序或任何其他顺序过程中,有时会出现一个短暂的状态不正确的时刻,希望稍后的指令能修正这种状态。他声称goto
语句会扰乱这种时间流程,使得程序的正确性难以判断。为了强调这一点,戴克斯特拉明确指出,“它[goto
语句]是使程序变得混乱的诱惑。”这篇文章通常被认为是“结构化编程”运动的开端。
结构化编程运动是对使用 goto
所带来的潜在危险的一种回应。这一运动证明了程序可以无需使用 goto
实现图灵完备, 并在编程语言方面取得了重大进展。他们推动创建了更加受限和可预测的编程语言。我们今天常用的许多编程关键字,如 while
、do
、break
、continue
等,都可以追溯到这一运动。该运动取得了成功,今天我们广泛使用的许多语言(如 C、Python、Java)是结构化编程的成果。
今天每个学计算机的学生都被教育要远离 goto
,就像躲瘟疫一样。而在现代编程里几乎看不到它的身影。不过,有一个例外值得一提。
goto
在 Linux 内核中其实很流行
你知道吗?goto
在 Linux 内核中其实很流行
尽管有结构化编程运动以及像迪杰斯特拉这样的专家几十年来的建议,仍然有一群高产且非常资深的程序员不会让那些脱离实际的学术人士来告诉他们应该怎样编程。
林纳斯·托瓦兹,就是这样一位Linux 内核的创造者。当一位贡献者直接要求他移除内核代码中的 goto
,而这位贡献者一直被教导认为 goto
语句是“本质上是邪恶的”时,林纳斯回答说:“不,你被计算机科学家洗脑了,他们认为尼古拉斯·维特知道自己在说什么。”尼古拉斯·维特是结构化编程的倡导者之一和 Pascal 语言的创造者(林纳斯特别不喜欢这种语言)。阅读完整对话在这里:这里。
程序员们在哪些情况下会使用 goto
?为什么他们还会继续使用,尽管编程社区普遍建议不要使用它?
其中一个例子是错误处理及资源清理。在内核空间中,你面临许多需要清理的资源。一旦出错,资源可能处于的状态多种多样,你需要妥善处理这些资源。goto
提供了一个简单而优雅的解决方案来应对这个问题。
以下是从 Alessandro Rubini、Greg Kroah-Hartman 和 Jonathan Corbet 合著的《Linux 设备驱动程序》第二章中的一个 C 代码示例,展示如何使用 goto
进行错误处理在虚构的例子中:
// 原代码
int _init my_initfunction(void)
{
int err;
/* 注册需要一个指针和一个名称作为参数 */
err = register_this(ptr1, "skull");
if (err) goto fail_this;
err = register_that(ptr2, "skull");
if (err) goto fail_that;
err = register_those(ptr3, "skull");
if (err) goto fail_those;
return 0; /* 成功完成 */
// 在发生错误时,程序将跳转到失败标签
fail_those: unregisterthat(ptr2, "skull");
fail_that: unregisterthis(ptr1, "skull");
fail_this: return err; /* 继续传递错误 */
}
可以看到每个可能的错误状态都有一个标签,并且有一个 goto
语句用来跳转到相应的标签。如果出现错误,程序将跳转到相应的标签并释放已分配的资源。这是解决复杂问题的一种简单而优雅的方法。
考虑没有 goto
的这个例子
int _init my_init_function(void)
{
int err;
err = 注册此(ptr1, "skull");
if (err) {
return err; /* 返回错误继续处理 */
}
err = 注册那个(ptr2, "skull");
if (err) {
注销此(ptr1, "skull");
return err; /* 返回错误继续处理 */
}
err = 注册那些(ptr3, "skull");
if (err) {
注销那个(ptr2, "skull");
注销此(ptr1, "skull");
return err; /* 返回错误继续处理 */
}
return 0; /* 成功返回 */
}
这是一个非常小的例子,即使在这么小的例子中,代码重复的程度依然令人震惊。你可以看到代码很快变得更为复杂且难以阅读。如果要重构以包含更多资源分配,几乎需要重写整个函数。
虽然在这个例子中goto
很优雅,它依旧是一个颇具争议的选择。许多程序员可能会说你应该尽量使用结构性编程并避免使用goto
。在这种情况下,在上述例子中,有什么可以替代goto
的选择吗?
最后我们来看看Zig。Zig 是一种系统编程语言,正在编程社区中迅速流行起来。它旨在简单、高效且可预测。它的设计目标是成为更好的C语言替代品。Zig 可能比C语言更好的地方之一是它的错误处理。
Zig 提供了两个语句直接解决了错误处理和资源清理问题,这些问题在 Linux 内核中经常通过使用 goto
语句来解决。这两个语句是 defer
和 errdefer
。这些语句让你可以在当前作用域结束时延迟执行一段代码。结合 Zig 的 error
类型,你可以写出既简单又优雅的代码。
我们来看看怎么用Zig的errdefer
语句改写之前的例子:
fn myinitfunction(ptr1: *u8, ptr2: *u8, ptr3: *u8) !void {
// 注册第一个资源
try register_this(ptr1, "skull");
// 如果出错,就取消注册这个资源
errdefer unregister_this(ptr1, "skull");
// 注册第二个资源
try register_that(ptr2, "skull");
// 如果出错,取消注册这个资源
errdefer unregister_that(ptr2, "skull");
// 再试试注册那些
try register_those(ptr3, "skull");
}
这里,一旦出现错误,errdefer
语句会在当前作用域结束时运行 unregister_this
函数。用 errdefer
包围的代码块会像一个后进先出(LIFO)栈一样工作。一旦出错,最近的 errdefer
语句及其之后的所有语句会被执行。这种行为完美地模仿了之前示例中 goto
的预期效果。
对于不熟悉 Zig 的朋友来说,!
符号表示该函数可能会返回一个错误。try
语句会尝试执行后面的表达式,如果该表达式返回错误,则会将错误向上抛出调用栈。更多详情请参阅 这里。
看看这段代码多干净、多易读。在这里,我们可以在函数结束时用 errdefer
在出错时推迟资源清理,而无需任何 goto
语句。
Zig 中的 defer
和 errdefer
之间的区别很重要。defer
会在当前作用域结束前执行代码块,无论是否发生了错误。而 errdefer
只在发生错误时才会执行代码块。示例中,文件的内容决定了需要注册哪些资源。
fn myinitfunction(ptr1: *u8, ptr2: *u8, ptr3: *u8) !void {
var fs = std.fs.cwd();
// 打开文件
var file = try fs.openFile("resources.txt", .{});
// 在函数结束前关闭文件
defer { file.close(); }
// 设置一个字节缓冲区来读取文件内容
var buf: [100]u8 = undefined;
const bytes_read = try file.readAll(&buf); // 读取文件内容到字节缓冲区
// 根据文件内容决定是否注册资源
const register_this: bool = bytes_read > 1 && buf[0] == 'y';
const register_that: bool = bytes_read > 2 && buf[1] == 'y';
const register_those: bool = bytes_read > 3 && buf[2] == 'y';
// 根据文件内容注册资源
if (register_this) { try registerthis(ptr1, "skull"); }
// 创建条件错误处理语句,在出错时取消注册已注册的资源
errdefer if (register_this) unregisterthis(ptr1, "skull");
if (register_that) { try registerthat(ptr2, "skull"); }
errdefer if (register_that) unregisterthat(ptr2, "skull");
if (register_those) { try registerthose(ptr3, "skull"); }
errdefer if (register_those) unregisterthose(ptr3, "skull");
}
在这里,我们有与之前相同的模式,但增加了从文件中读取以确定应注册哪些资源的复杂度。就像之前一样,errdefer
语句也用于根据文件内容注销注册的资源,但在这种情况下,这些 errdefer
语句是条件性的,确保只清理已注册的资源。作为额外的好处,不管是否出错,defer
语句都会关闭文件。
用 goto
来实现这种行为将会是一场噩梦。你肯定得复制代码,并且得小心地跟踪资源的状态。Zig 的 defer
和 errdefer
命令为这个问题提供了一个既简单又优雅的解决方案,易于理解和阅读。
了解有关Zig的defer
和errdefer
语句的更多信息,请参阅这篇文章博客文章。
所以,Zig 最终彻底放弃了 goto
吗?很难说。Linux 内核开发者们很固执,他们不太可能因为一种新的编程语言而改变主意。然而,看到旧问题的新解决方法还是很令人振奋的。Zig 作为系统编程语言仍然充满潜力,它出色的错误处理只是众多吸引开发者们注意这种令人兴奋的新语言的原因之一。
你对goto
有什么看法?你认为是时候和祖父最喜爱的关键词之一说再见了吗?Zig在所有情况下都有提供goto
的替代方案吗?你能否举个例子说明goto
依然有优势的情况?请在下面的评论区分享你的想法。
我叫亚历克斯·吉尔伯特,在接下来的文章里,我将更深入地探讨Zig的defer
和errdefer
语句,并比较使用这些语句与使用try
/catch
(异常处理机制)和其他常见错误处理机制的运行时的后果。如果你对了解Zig以及它如何改变我们编写系统软件的方式感兴趣,记得关注我在Medium上的文章。
共同學(xué)習(xí),寫(xiě)下你的評(píng)論
評(píng)論加載中...
作者其他優(yōu)質(zhì)文章
100積分直接送
付費(fèi)專(zhuān)欄免費(fèi)學(xué)
大額優(yōu)惠券免費(fèi)領(lǐng)