上个月我开启了一个新项目,抱着实验的心态用起了Rust,因为:

  • 社区里有很多人号称Rust is ready for web。
  • Rust连续若干年蝉联Stackoverflow上最受使用者欢迎的语言。

这篇小文总结了我最近对Rust的体验。 注:本文的评论仅对Rust 2020有效。

诚然,Rust并非浪得虚名,它的优点还是不少的。 在语言层面上,它对表达式的理解是进步的, 整体的语法是有很强的一致性,宏带来了足够的表达力,Trait的设计基本完善。 在工具链上,我个人很喜欢Cargo的体验,其对Monorepo的原生支持很赞。 Rustup也很方便。RLS有一些缺陷,但是基本上可以接受。 Rust编译器本身的错误提示不完美,但是每个错误都有对应的文章解释原因,让人印象深刻。 社区文化上,Rust社区对于文档的重视可以体现在它的血液里,发布package就自动生成文档网站真是一件非常方便和超前的事情, 非但如此,在文档中写的代码是被视为测试代码的存在,需要通过编译才可以发布,酷不酷? 另外,Rust的用户论坛上发问题得到回答的速度相当惊人的快,

然而,硬币总有两面,事实上我觉得Rust的体验在服务端开发的场景下并不友善,以至于使我不推荐它。

首先就是工程师的心智负担,完全消除GC是一个宏伟丰满的梦想,但我认为现实太骨感,用borrow/ownership的模式下有太多边际条件需要用奇怪的方法来解决。 其实在大部分程序里,这些边际条件都没什么太大的问题,但在服务端开发就不一样了。 比如Rust的闭包设计,Rust闭包允许用引用或者捕捉外部变量,这在通常情况下是可以的,但是在多线程的情况下,这种简单的模式就会出各种问题。 因为闭包本身的使用模式可以很丰富,一个闭包的生命周期可能会持续到程序的结尾,而它可能被多次调用,这时候,不论是“捕捉”还是“引用”都无法制作一个合适的闭包。 这个时候需要推出一个“拷贝”的闭包模式,但显然这个不是无代价的抽象,恐怕这几年都不会出现。 再比如因为Rust使用栈来管理变量的解构,使很多工具都用Drop来实现shutdown逻辑,然而这其实是个倒退,这降低了生命周期管理的粒度。 除此之外,你还可以经常看到'static在rust的工具里被过度使用,哪怕这完全是不需要的,感觉有削足适履的感觉。 从宏观来看,我认为GC还是利大于弊,尤其是在服务端应用里,遍地的堆内存状态。 我盼望如果能像是optional type一样,拥有Optional的No GC能力就好了。

另一大当前缺陷在我来看就是Rust的async和sync版本彼此不兼容以及async本身的不成熟。 现在基本没有争议的事是,在普遍的场景下async(用户空间轻量级进程)的性能更好。 Rust的async feature是在去年才引入stable的,由于不兼容sync,现在Rust的Web社区基本分为了两派,async和sync,这是一个很无奈的状况。 很多积攒多年的工具都是基于sync来做的,一下子引入的async之后,这些工具都得被迫大重构,而且还得被迫放弃一半community。 因此作为工具作者基本处于两难的状况。 然而如果async Rust本身足够成熟,这问题也不会那么严重,我认为现在一个很大的问题是Rust无法在不依赖特定runtime的情况下spawn async task, 详见这里这里。 这个问题可能(已经)会导致更大的分裂,也催生了些奇趣的工具。 有时候我想,如果把Rust sync和Rust async都视为一种Runtime,尽可能实现同构的API,是否就可以让世上出现兼容sync和async的工具框架? 不管如何,直到Rust async成熟,要想有好体验估计还有很久。

最后就是一些细枝末节的细节:

  • 指针类型具有特别的casting能力,但是文档没有明确指出。
  • 很多一些所谓高级的语言内容,Unpin/Pin以及Exterior Mutability,UnwindSafty,都是很容易遭遇的概念,然而很多遭遇都是被动遭遇,这对开发体验影响很大。
  • 工具多样性,Rust还是缺一些web的必要工具,比如针对中大型项目的IoC/DI框架。
  • Async Rust无法访问调用栈的上下文,这把一些很重要的动态功能给挡在了门外,例如根据根据上下文来创建日志。
  • 缺乏一个正常的的union type,感觉这是从Haskell里学来的坏东西。
  • 社区过度依赖宏。
  • 对编译时间的控制非常困难,稍有不慎项目的编译时间就会skyrocket。

由于以上诸多缺点,我也许不会继续使用Rust在我的项目里,但尽管如此,我还是在长期来看还是很看好Rust的。 正是因此我制作了Sai,希望能弥补社区的空白, 并且也许能帮助到想用Rust来开发大规模服务端程序的开发者。