坦白说,我(笔者)在写这篇文章之前犹豫了很久,我怕文章最终沦为各方编程语言势力口诛笔伐的导火索。但确实有很多朋友会问我Rust用起来怎么样、要不要在他们自己的项目里选择Rust。思来想去,我最终还是决定分享一些初创公司环境下Rust的使用心得,聊聊这种语言跟快速行动、扩展团队这两大核心目标间的冲突。
我其实挺喜欢Rust,也绝无抹黑Rust的意思。但亲身经历告诉我,选择Rust几乎必然会对生产力造成重大影响,影响到快速行动这个基本目标。所以动手之前,请大家认真权衡Rust造成的速度影响到底能不能抵得上它给业务和产品带来的收益。
Rust是那种优缺点都很明显的语言。如果大家的项目需要Rust的某种特性,如高性能、超强类型、无垃圾收集的系统语言等,那它确实表现很棒。但Rust也有相当不擅长的场景,即团队为Rust的复杂性和编写开销付出了代价,却得不到什么好处。
我对Rust的主要体验来自之前在一家初创公司的从业经历,前后大概持续了两年多。那是一个基于云的SaaS项目,也算是一个基于传统CRUD的应用程序:包含一组微服务,在数据库前提供REST和gRPCAPI端点,外加一些后端微服务(本身是用Rust和Python混合实现的)。
之所以使用Rust,是因为公司的几位创始人都是Rust专家。但随着时间推移,我们的团队规模显著扩大(工程人员数量增长了近10倍),代码库的规模和复杂度也随之快速提升。
随着团队和代码库的并行膨胀,我意识到公司为了继续使用Rust而付出了更高的代价。有时候开发速度非常缓慢,新功能的发布时间也比我预期得更长,人们都感觉到当初选择Rust的决定并不利于释放生产力。
从长远来看,换一种语言重写代码反而能让开发更灵活、加快交付时间,但问题是我们难以抽出完整的时间来推进这项重写工作。总之,我们有点被Rust困住了的感觉,任何解决办法都没那么容易实现,我们需要硬着头皮强上。
这些情况好像跟很多朋友的固有印象不同。那么,既然Rust语言如此安全稳定、性能卓越,怎么在我们这就不灵了呢?
陡峭的学习曲线
在整个职业生涯里,我接触过几十种语言,而且大部分都是现代的过程化语言(例如C++、Go、Python和Java等),它们的基本概念都高度相似。虽然每种语言之间也有差异,但主要体现为跨语言的模式区别,掌握起来并不太困难。
但对于Rust来说,我发现最困难的其实是接纳一整套全新的思路——比如生命周期、所有权和借用检查器等。即使是经验丰富的编程老鸟对这些定义也并不熟悉,因此必然会面对一段颇为陡峭的学习曲线。
当然,其中一些“新”思路在其他语言中也有体现,特别是在函数式语言当中。但Rust是第一次将其全面引入“主流”语言环境,自然令众多Rust新手苦不堪言。虽然我的同事们都非常聪明、也很有开发经验,但包括我自己在内的很多人都难以理解Rust中的某些实现规范、看不懂编译器中为何经常出现神秘的错误消息,也不明白关键库的工作原理。
为此,我们开始每周为团队举办“Rust学习会”,分享知识和专业意见。但开发速度放缓已成事实,这极大削减了团队的生产力和士气。
作为鲜明的对照,我当初在谷歌工作时曾从C++全面转向Go语言,整个过程只用了不到两周时间,十五人的团队在首次编写Go程序时感觉相当轻松。但在Rust上,即使是在全职开发了几个月后,团队中的大多数成员还是觉得很没有信心。不少开发者告诉我,他们心里感觉很难受,因为功能实现所需要的时间比他们预期要长,而这一切都源自他们被迫以Rust的方式去思考。
Rust并不特别,总有语言可以替代它
如上所述,我们构建的服务是一个比较简单的CRUD应用。在这套系统的整个生命周期中,服务的预期负载不会超过每秒几条查询,但该服务背后是一条相当复杂的数据处理管道,可能需要几个小时才能运行起来,所以该服务应该不会成为性能瓶颈。换句话说,就算是Python这类并不以性能见长的语言,在这样的场景下也绝对不会惹出什么麻烦。
另外,除了面向Web的服务需求处理之外,该服务也没有任何特别的安全或并发需求。我们之所以使用Rust,唯一的理由就是这套系统的发起者是Rust专家,跟语言本身的特性和适应性毫无关系。
Rust语言有个著名的设计权衡——安全性比开发生产力更重要。在很多场景下,这样的决断并没有问题:无论是在操作系统内核里构建代码,还是限制嵌入式系统的内存,这都很有必要。但除此之外,还有一些没必要那么在意安全的需求,特别是对于我们这样以速度决成败的初创公司。
我是典型的实用主义者,宁愿让团队花时间调试Python或Go代码中偶尔出现的内存泄漏或类型错误,也不愿让每个人都为了彻底回避这些问题而被迫将生产效率降低四分之三。
前文已经提到,我曾在谷歌团队用Go语言构建过一项服务。随着时间推移,该服务已经支持超过8亿用户,而且峰值期流量是谷歌搜索QPS的4倍。在构建和运行该服务的那些年里,由Go类型系统或垃圾收集器引起问题的次数其实一只手就数得过来。
基本上,Rust所解决的问题用其他办法也能搞定——比如更好的测试、更好的linting、更好的代码审查和监控等。除非实在拿不出这些资源,Rust才可能是个不错的选择。
很难招聘到Rust开发人员
在这家公司供职期间,我们雇用了不少员工。但在60多人的工程团队里,只有两三位此前有过Rust开发经验。不是我们不愿意找有经验的Rust程序员,而是实在找不到。
所以大家在做决定之前一定要慎重,纯Rust开发其实真的不太符合创业环境的基本需求。没错,随着Rust变得愈发主流,相应开发人才肯定会越来越多,但至少就目前来看,这事并不容易。
还有另一个次要因素就是,团队中懂Rust的成员和不懂Rust的成员之间出现了割裂。由于这是一种较为“深奥”的编程语言,所以公司里原本能帮助构建功能、调试生产问题的其他工程师完全插不上手。这时候如果希望快速行动、发挥团队中每位成员的综合优势,Rust就成了横亘在面前的最大障碍。
根据我的经验,程序员在C++和Python之类的语言间其实可以轻松切换,但Rust太新、也太复杂了,拉高了人们之间的协作门槛。
库和说明文档都不成熟
我希望这个问题能逐步得到解决。但必须承认,跟Go语言相比,Rust的库和文档生态系统都非常不成熟。
Go的优势在于,其对外发布的一切成果都由谷歌的专项团队开发和支持,所以配套的文档和库也相当完善。相比之下,Rust总有种“半成品”的感觉,库和说明文档严重缺失,人们往往需要查看库的源代码才能理解如何使用。这不好,非常不好。
Rust的拥护者们倒也承认“async/await还不成熟”以及“库说明文档确实不完备”,但光承认没用,这些问题实实在在影响了团队的开发效率。
我们早期采用Actix作为服务的Web框架时就犯了大错,后面引发了不少麻烦。这个库里深藏着各种Bug和问题,直到现在也不清应该如何修复。(当然,这是几年前的事了,也许现在情况已经有所改善。)
当然,这些问题也不是Rust特有的,但却成了开发团队无法回避的“技术税”。无论核心文档和教程有多么出色,如果不知道该怎么用那些库,一切都将毫无意义。
很难用Rust粗略地构建一项新功能
我不知道其他人怎么看,但就我个人而言,我在构建新功能前肯定不会做好数据类型、API和其他所有细节准备。我一般是先把代码写下来,试试基本思路行不行得通。
在Python中执行这类操作非常容易,我们可以快速尝试、随意探索,不用担心粗略的构思会影响到某些代码路径。在确定行得通后,我们可以再回头整理内容、修复类型错误、编写测试,完全不耽误。
但在Rust中,这种“粗糙编码”的方式就非常困难,因为编译器可能会觉得每个不符合类型和生命周期检查的问题都是错误。这就是Rust的特点——设计必须明确。
如果我们需要构建的是生产就绪型产品,那这很对、很好。但如果只是想做点测试或者初步探索,那这样的特性就太糟糕了。虽然也能帮上点忙,但还是需要在编译之前对堆栈上下的所有内容进行类型检查。
更麻烦的是,当需要更改承载接口的类型签名时,我们会发现自己要耗费几个小时来变更各个使用到该类型的位置后,才能弄清最初的尝试可不可行。如果需要再做调整,那整个过程还得重新来一次。
结束语
Rust肯定有一些我喜欢的东西,我也希望它的一些特性能被其他语言所吸纳。首先就是匹配语法,另外还有强大的Option、Result和Error特性,“?”运算符能够优雅地处理错误。这些设计在其他语言中也有类似的体现,但Rust从上到下都透着一股子优雅。
对于那些需要高性能、高安全性的项目,我绝对愿意使用Rust。毕竟在这类项目里,我应该不会频繁修改项目的主体部分,对于速度也没有太高的要求。但在小型团队里,就不太适合全面使用Rust了——用来开发内核模块、固件、游戏引擎等是好的,毕竟它们都强调性能和安全性,而且在发布前往往难以进行彻底测试。但除此之外,请千万慎用Rust,这语言可不好惹!
原文链接: