Rust初体验

Rust 作为一个新兴语言经常被拿来跟 Golang 比较,我又是个 Golang 爱好者,所以从大概去年这个时候开始关注 Rust。不过因为 Rust 的语法比较复杂,所以始终也没有系统地过一遍。恰好最近 o’reilly 家非常良心地出了一本 Rust 的免费电子书,叫《Why Rust?》,只有62页,我就抽周五无聊的电路实验闲暇把它看了一遍,有些比较感兴趣的部分又看了看官方的《The Rust Book》,算是对 Rust 有了一个系统的了解,就随手写一下我的感受。

本文主要是几个我比较关心的方面 Rust 与 Golang 的比较。毕竟我没怎么用过 Rust,所以这篇文章也许有偏颇之处,欢迎理性讨论。

内存管理

我想 Rust 最被人称道的就是它的所有权(ownership)系统。《Why Rust?》中总结所有权系统有三条法则:

  • 每个值(value)只有一个所有者(owner)。
  • 值的引用可以被借走,但是借出去的这个引用的生命周期不能比这个值的生命周期长。
  • 只有当你有对值的互斥访问权(owner 或 借到了引用),你才能操作这个值。

我理解,所有权系统实际就是保证了当内存被释放时所有指向这部分内存的变量一定都已经出了生命期了。再加上 Rust 直接取消了空指针(null)类型,就保证了不会出现内存泄漏的问题。另一方面,由于 Rust 有严格的类型系统,不能对指针随意加减,这样就彻底消除了段错误问题。

更令我惊奇的是,借助这一套系统,Rust 能够部分解决多线程的竞态问题,因为竞态的本质就是同时对一个变量进行多个操作,而这个情况是不符合所有权系统的。

这个设计确实很巧妙。虽然可能加重了程序员的思考负担,但是由于 Rust 的编译器错误提示很人性化,所以其实也没什么。

而 Golang 是有垃圾收集的。Go 1.5版本实现了并行 GC,保证每50ms中最多只有10ms STW。我认为这个性能指标已经非常出色了。

内存管理方面两者没什么可比性,有没有 GC 区别太大了,各有好处。GC 能够很大地减轻程序员的负担,提高开发效率,但是有延迟,所以实时系统没法用。而且有些需要对内存精细管理的场合(嵌入式)不能用 GC。对我来说,我更喜欢 Golang 的设计,因为我没有上面所说的需求。

并发

在并发方面,除了前文所说的所有权系统部分减轻竞态问题之外,Rust 并无亮点。Rust 几乎没有任何用于高并发的语言基础设施。Rust 的并发模型是 Actor 模型,也提供了类似 channel 的通道,但是由于轻量级线程 libgreen 库被砍掉了,所以只能用系统线程。第三方库中名气较大的 mio 我也看了一下,感觉还是直接的事件循环写法,没有什么封装或者语法糖,显然不如 Golang 或 Erlang 的直接同步写法舒服。

Golang 几乎可以说为并发而生,提供了丰富的内建基础设施。这里的“内建基础设施”远不止是 Goroutine、select 和 channel, Golang 的标准库也是完全与 Goroutine 相适配的全异步写法(原本会阻塞的地方出让控制权,直到返回结果)。类似功能的第三方库比如 Python 的 Gevent 库需要打猴子补丁才能做到这一点。静态编译语言没法打猴子补丁,所以 Rust 未来通过第三方库来解决这个问题肯定不如 Golang 的语言内建特性优雅、好用。还记得 Node 的作者为什么选择 Javascript 吗?

泛型、接口

Rust 的泛型并不复杂,可以指定简单的类型参数,也可以指定类型必须实现的 trait,同时支持类似 mixin 的写法。Rust 中没有传统的接口,但是有 trait,trait 不仅可以指定方法也可以指定成员变量,我觉得 trait 更类似于 C++ 中的抽象类。

据传说 Golang 2.0中可能会引入泛型(好像是在 Hacker News 上看到的?),官方博客上也说泛型这件事还在考虑中,但是现在毕竟是没有,所以 Rust 完爆 Golang。我很喜欢 Rust 的泛型机制,如果 Golang 也能实现类似的就很好了。毕竟,工程中确实是有写泛型容器的需求。如果用基于go generate模板替换一类的方法要生成巨多的重复代码,如果用我之前博客介绍过的基于接口的变通方案也不够自然。

Golang 中不引入泛型还有一个原因就是泛型会降低编译速度。看 Golang 团队如何权衡吧。

错误处理

Rust 把错误分为失败(failure)和恐慌(panic),区别就是失败可以恢复,恐慌不能回复。借助灵活的模式匹配和 Option、Result类型,Rust 中失败的写法很优雅。如果一个函数可能会失败,就让它返回 Option 或者 Result,然后用模式匹配判断返回值是不是 Nothing 或 Err。我感觉这个做法实际是借鉴了 Haskell 的 Monad。恐慌就会直接导致线程崩溃,令我费解的是恐慌竟然是没法捕获的,没法 catch 或者 recover。Rust 没有异常。

Golang 把错误分为错误和恐慌,但是由于没有模式匹配、Option 和 Result,Golang 通过多返回值返回错误,然后在调用方检查错误是否为 nil。形式上我认为 Rust 更为优雅,但是本质上我感觉没有区别。Golang 的恐慌可以 recover,我认为这点比 Rust 要好,有些时候一个错误需要传到外层恢复,Golang 中可以在若干层外 recover,不会导致程序挂掉,而 Rust 只能把失败一层一层往外传,这是不是麻烦了一点?

结语

对我来说 Rust 在语法上有很多吸引力,比如泛型、模式匹配。但是由于 Rust 没有 GC(基本可以肯定开发效率会低),并发方面不如 Golang 完善,所以我不会完全迁移到 Rust 上。

Go, Rust