Skip to content

【Zig 日报】关于 Go、Rust 和 Zig 编程语言的思考 #291

@jiacai2050

Description

@jiacai2050

我最近意识到,我倾向于使用“手头上的工具”而不是“最合适的工具”,这很大程度上决定了我掌握的编程语言。因此,在过去的几个月里,我花了很多时间尝试工作中不常用的语言。我的目标不是精通,而是想对每种语言的优势形成自己的看法。

编程语言的差异体现在诸多方面,比较它们很容易陷入“各有优劣”这种显而易见却无甚帮助的结论。当然,存在权衡。更重要的问题是:为什么这种语言会选择这种特定的权衡方式?

这个问题很有趣,因为我不想像购买加湿器那样,根据功能列表来选择语言。我关心软件构建,也关心我的工具。语言在做出权衡时,表达了一系列价值观。我想找到与我产生共鸣的价值观。

这个问题也有助于区分那些最终功能集高度重叠的语言。如果网上关于“Go vs. Rust”或“Rust vs. Zig”的讨论数量是一个可靠的指标,那么人们显然感到困惑。记住语言X因为拥有a、b和c功能而更适合编写Web服务,而语言Y只有a和b功能,这很困难。更简单的是,记住语言X更适合编写Web服务,是因为语言Y的设计者讨厌互联网(假设),并认为我们应该拔掉插头。

我在这里收集了我最近尝试过的三种语言的印象:Go、Rust和Zig。我试图将我对每种语言的体验提炼成对该语言价值观及其执行情况的全面判断。这可能过于简化,但我的目标正是提炼出一些简化的偏见。

Go

Go的特点是极简主义。它被描述为“现代C”。Go不像C,因为它具有垃圾回收机制和真正的运行时,但它像C一样,整个语言都可以装在你的脑子里。

你可以将整个语言装在脑子里,因为Go的功能非常少。长期以来,Go因没有泛型而臭名昭著。这在Go 1.18中终于改变了,但那是在人们乞求添加泛型12年后才实现的。其他现代语言中常见的特性,如标记联合或错误处理的语法糖,都没有被添加到Go中。

Go开发团队似乎对添加新功能有很高的门槛。最终结果是一种语言,它迫使你编写大量的样板代码来实现可以用其他语言更简洁地表达的逻辑。但结果也是一种随时间推移稳定且易于阅读的语言。

例如,考虑Go的slice类型。Rust和Zig都有slice类型,但它们只是胖指针。在Go中,slice是指向内存中连续序列的胖指针,但slice也可以增长,这意味着它涵盖了Rust的Vec类型和Zig的ArrayList的功能。此外,由于Go为你管理内存,Go会决定slice的底层内存是位于栈上还是堆上;在Rust或Zig中,你必须更深入地思考内存的位置。

Go的起源神话,据我所知,基本上是这样的:Rob Pike厌倦了等待C++项目编译,并且厌倦了Google的其他程序员在这些C++项目中犯错。因此,Go在简单性方面超越了C++的繁琐。它是一种为编程人员设计的语言,足以满足90%的使用情况,并且易于理解,即使(甚至特别)在编写并发代码时也是如此。

我工作中不用Go,但我认为我应该使用它。Go的极简主义服务于企业协作。我不是在贬低它——在企业环境中构建软件有其自身的挑战,Go可以解决这些挑战。

Rust

Go是极简主义者,而Rust是最大主义者。Rust经常被使用的标语是“零成本抽象”。我将修改为“零成本抽象,以及大量的抽象!”

Rust以难以学习而闻名。我同意Jamie Brandon的观点,即让Rust变得困难的不是生命周期,而是语言中塞入的概念数量。我不是第一个批评这个Github评论的人,但它完美地说明了Rust的概念密度:

Pin<&LocalType> 类型实现了 Deref<Target = LocalType>,但没有实现 DerefMutPin& 标记为 #[fundamental],以便有可能实现 Pin<&LocalType>DerefMut。你可以使用 LocalType == SomeLocalStructLocalType == dyn LocalTrait,并且可以将 Pin<Pin<&SomeLocalStruct>> 强制转换为 Pin<Pin<&dyn LocalTrait>>。(实际上是两层 Pin!) 这允许在稳定版上创建一对“实现 CoerceUnsized 但行为奇怪的智能指针”(Pin<&SomeLocalStruct>Pin<&dyn LocalTrait> 成为具有“奇怪行为”的智能指针,并且它们已经实现了 CoerceUnsized)。

解释一些关键术语:

  • Pin: Rust 中的一个类型,用于确保指针在内存中不会被移动,这对于不安全的代码和某些性能优化至关重要。
  • Deref: 一个 trait,允许类型像解引用一样访问其内部值。
  • DerefMut: Deref 的可变版本,允许类型像解引用一样访问其内部值的可变引用。
  • &: 引用,指向数据的借用。
  • #[fundamental]: 一个标记,表明该类型是 Rust 语言的基础类型,可以用于某些特殊操作。
  • CoerceUnsized: 一个 trait,允许将一个类型强制转换为另一个类型,即使它们的大小不同。
  • SomeLocalStruct: 一个具体的本地结构体类型。
  • dyn LocalTrait: 一个动态 trait 对象,表示任何实现了 LocalTrait 的类型。
  • 智能指针: 一种行为类似于指针,但提供额外功能的类型,例如自动内存管理。

总而言之,这段文字描述了在 Rust 中使用 Pin 和引用时遇到的一个限制,以及如何通过一些技巧(例如使用多层 Pin 和动态 trait 对象)来绕过这个限制,并创建具有特定行为的智能指针。

当然,Rust试图做到最大主义的方式与Go试图做到极简主义的方式不同。Rust是一种复杂的语言,因为它试图实现两个目标——安全性和性能——这两个目标在某种程度上存在冲突。性能目标不言而喻。“安全”的含义不太清楚;至少对我来说是这样,也许我只是被Python洗脑太久了。“安全”意味着“内存安全”,即你不应该能够取消引用无效指针,或者进行双重释放等操作。但它也意味着更多。一个“安全”程序避免了所有未定义行为(有时称为“UB”)。

什么是可怕的UB?我认为理解它的最好方法是记住,对于任何正在运行的程序,都有比死亡更糟糕的命运。如果你的程序中出现问题,立即终止实际上很好!因为另一种选择是,如果错误没有被捕获,你的程序会进入一个不可预测的暮光区,其行为可能由下一个数据竞争获胜的线程或特定内存地址上的垃圾决定。现在你有海森堡错误和安全漏洞。非常糟糕。

Rust试图在不付出任何运行时性能代价的情况下防止UB,方法是在编译时检查它。Rust编译器很聪明,但并非全知全能。为了能够检查你的代码,它必须理解你的代码在运行时会做什么。因此,Rust具有表现力强的类型系统和各种特征,这些特征允许你向编译器表达,在其他语言中只会是代码的明显运行时行为。这使得Rust变得困难,因为你不能只是做这件事!你必须找到Rust对这件事的称呼——找到所需的特征或任何东西——然后按照Rust的期望来实现它。但是,如果你这样做,Rust可以对你的代码的行为做出其他语言无法做出的保证,这对于你的应用程序来说可能至关重要。它还可以对其他人的代码做出保证,这使得在Rust中使用库变得容易,并解释了为什么Rust项目几乎和JavaScript生态系统中的项目一样多的依赖项。

Zig

在这三种语言中,Zig是最新的,也是最不成熟的。截至目前,Zig只有0.14版本。它的标准库几乎没有文档,学习如何使用它的最佳方法是直接查阅源代码。

虽然我不知道这是否属实,但我喜欢将Zig视为对Go和Rust的一种反应。Go之所以简单,是因为它掩盖了计算机实际工作方式的细节。Rust之所以安全,是因为它迫使你跳过它的许多障碍。Zig将解放你!在Zig中,你控制着宇宙,没有人可以告诉你该做什么。

在Go和Rust中,在堆上分配一个对象就像从函数返回一个结构体指针一样简单。分配是隐式的。在Zig中,你必须自己分配每个字节,显式地。(Zig具有手动内存管理。)你在这里拥有比在C中更多的控制权:要分配字节,你必须在特定类型的分配器上调用alloc(),这意味着你必须为你的用例选择最佳的分配器实现。

在Rust中,创建可变全局变量很难,以至于有很长的论坛讨论如何做到这一点。在Zig中,你可以直接创建一个,没问题。

未定义行为在Zig中仍然很重要。Zig称之为“非法行为”。它试图在运行时检测到它,并在发生时使程序崩溃。对于那些可能担心这些检查的性能成本的人,Zig提供了四种不同的“发布模式”,你可以在构建程序时选择其中一种。在其中一些模式中,检查被禁用。这个想法似乎是,你可以多次在经过检查的发布模式下运行你的程序,以合理地确信未检查构建的程序中不会出现非法行为。这对我来说是一个非常务实的设计。

Zig与另外两种语言的另一个区别在于Zig与面向对象编程的关系。OOP已经过时了一段时间了,Go和Rust都避免了类继承。但是Go和Rust对其他面向对象编程范例的支持足够多,以至于如果你想这样做,仍然可以将你的程序构建为相互交互的对象的图。Zig具有方法,但没有私有结构体字段,也没有实现运行时多态性的语言特性(也称为动态分派),即使std.mem.Allocator渴望成为一个接口。据我所知,这些排除是故意的;Zig是一种用于数据导向设计的语言。

还有一件事我想说一下,因为我发现它很有启发性:在2025年构建一种具有手动内存管理的编程语言似乎很疯狂,特别是当Rust已经表明你甚至不需要垃圾回收,并且可以让编译器为你做这件事时。但这是一种与排除OOP功能相关的设计选择。在Go和Rust以及许多其他语言中,你倾向于为对象图中的每个对象一次分配一小块内存。你的程序有成千上万个小隐藏的malloc()和free(),因此有成千上万个不同的生命周期。这就是RAII。在Zig中,手动内存管理似乎需要大量的繁琐且容易出错的簿记,但这只有当你坚持将内存分配与所有小对象联系起来时才成立。相反,你可以只在程序的某些合理点分配和释放大块内存(例如,在事件循环的每次迭代开始时),并使用该内存来保存你需要操作的数据。Zig鼓励这种方法。

许多人似乎对为什么Zig应该存在如果Rust已经存在感到困惑。这不仅仅是因为Zig试图变得更简单。我认为这种差异更为重要。Zig希望你从代码中消除更多的面向对象思维。

Zig有一种有趣、颠覆性的感觉。它是一种用于粉碎企业类层次结构(对象)的语言。它是一种为妄想狂和无政府主义者设计的语言。我喜欢它。我希望它能尽快发布一个稳定的版本,尽管Zig团队目前的重点似乎是重写所有依赖项。他们甚至可能在看到Zig 1.0之前尝试重写Linux内核。

Thoughts on Go vs. Rust vs. Zig | Sinclair Target

加入我们

Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来:

  1. 供稿,分享自己使用 Zig 的心得
  2. 改进 ZigCC 组织下的开源项目
  3. 加入微信群Telegram 群组

Metadata

Metadata

Assignees

No one assigned

    Labels

    日报daily report

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions