Rust语言68小时入坑体验

发布于 更新于
满怀期待地开始,摇头叹气地结束

《The Rust Programming Language》

我从《The Rust Programming Language》这本书开始入门学习rust语言。这本书讲的很详细,不仅讲了语法,还介绍了常见用法、一些约定俗成的习惯、甚至一些语言设计背后的思想。这个过程中我不仅仅是在学一门新的编程语言,我感觉我更像是在读一本小说、或者是在跟一位顶级程序员大佬闲聊,同时还在提升英语阅读能力。

我一共花了大约60个小时(从3月26日到5月3日一个多月的时间)才彻底过完这一本书,有一说一这个效率还是很低的(换成Go语言的话花60个小时我的代码都上生产环境了……)。因此如果赶时间、而且有足够的编程基础的话可以试试使用《Rust by Example》这个教程来学习入门。

我的大致学习时间表:

  • 花了25个小时学习基本语法(1~11章)
  • 5个小时实战项目(第12章:一个命令行工具程序)
  • 5个小时继续学习语法(13~15章)
  • 感觉(第15章)智能指针这块比较难以理解,花了10个小时写了几个算法题,包括数组、字符串、链表、二叉树等典型数据结构。
  • 7个小时继续学习语法(15~19章)
  • 7个小时实战项目(第20章:一个多线程web服务器)
  • (8个小时尝试重构第20章的作业,发现寸步难行,放弃了)

总结一下我对Rust的一些印象:

所有权

所有权Ownership和引用Reference这套机制是Rust语言最大的特征(就如go关键字对于Go语言一样),它也确实很强大。

虽然在C++的世界里,这种程序设计思想早已存在(哪怕是没正经写过C++的我都听说过),Rust所做的只是强硬地把这种思想明确下来并作为这门语言的核心特性而已。

不过即使是这样,(我认为)这依然达成了很好的效果,(Rust的所有权机制以及一些其他的规定)在研发成本可以接受的条件下达成了对底层的有力控制。

我个人还是很喜欢这种对内存布局的强力控制的,可能是因为背过Go的八股的缘故,我对底层的运行机制这块还是比较清醒的,因此哪怕即使我在写Python或者JS的时候,我也会自动在心里思考拷贝操作、对象生存周期、GC回收等方面的坑。之前写Go的时候由于不能明确指定堆栈分配,因此总会有点意犹未尽的感觉,如今写Rust正好能释放我这股洪荒之力,感觉还是挺舒畅的。

所有权由生命周期来控制,作为代价,我们还需要额外写『生命周期注解Lifetime annotation』。虽然实际开发过程中大部分生命周期可以省略,但是要理解它的概念还确实是有些费劲的。

继续深入,后面还有Box Rc RefCell Weak等更加复杂的引用类型,更是把这门语言的难度提升到了一个新的高度。

所有权的设计同时也导致了我们实际写代码过程中对变量的引用的行为会受到限制,比较典型的是在写二叉树数据结构的时候,类型定义Option<Rc<RefCell<TreeNode>>>这样一长串还是挺恐怖的。此外,由于编码难度的存在,会导致程序员产生某种特定的倾向——例如『递归 or 迭代』这个问题,在其他语言中也许区别不大,程序员们可以随便挑选喜欢的姿势,可在Rust语言中,则往往是递归会更容易写。

包管理

Rust的官方指定包管理工具是Cargo,对应的中心仓库是crates.io。跟Go比起来,Rust在这块我觉得是做得不好的。

一个最大的问题是,Rust的包的名字,没有携带组织名作为前缀。这就导致“抢注”的现象:例如在crates.io上搜索feishu关键字,排名第一的、占据了feishu这个包名的是一个啥都没有的空仓库,而且这个作者同时还抢注了其他多个经典名称,行为是有些恶劣的。这个问题我觉得其实很严重,但也许因为现在Rust还不够火、包还不够多,所以这个问题暂时还不算迫切吧,但我认为Cargo可能迟早需要一场巨大的改变来解决这个问题。

此外还有一些小问题。例如:在本地运行cargo命令行工具的时候,遇到一些奇怪的异常,还有它全量下载index的这个逻辑也让我无法理解,等等。

类型

『匹配match』这个东西一开始让我有些莫名其妙,为啥不用传统的switch关键字呢?等我学过之后发现它确实能力更加强大,我觉得挺好的,起个新名字也没大问题。

『枚举Enum』有个很舒服的特性是支持枚举类型,再加上宏、强大的泛型系统,使得Rust的类型系统表达能力令我满意(这点比Go强大太多了不得不承认,但是比Typescript还是差)。

『泛型generic』很经典。

『特征Trait』就是其他语言中常见的『接口interface』,但是稍有些不同:它不仅要求“长得一样”,还要求明确用impl for来显式指定实现了接口;此外同时还能附带额外的方法能力。

Rust的泛型和特征系统都特别灵活,正常能够想到的写法全部都能够实现,表达能力非常强。

特征系统与Go语言泛型草案中提到的『类型约束type constraint』是相同的思路,这种方式应该算是当下业界中比较主流的设计方式吧,应该不是谁抄谁的问题。

异常

Go的if err != nil虽然我个人觉得出了啰嗦之外没什么毛病,但是在舆论中来看,吐槽的人确实还是比支持的人更多的。

Rust的异常处理方案,其实某种程度上与Go的思路是一致的,不过它凭借宏、更强大的泛型、更严格的编译时检查,走出了一条完全不同的道路。

首先它对Result类型的match处理,其实是与Go的if err != nil半斤八两的,不过它还继续提供了?这个语法糖,立马就让语法变得清爽多了。

其次,它规定panic不可被恢复,也就是说强制要求程序做更多的检查判断以确保所有异常都形成Result而不是跟panic混在一起。对于特殊情况也提供了std::panic::catch_unwind工具来实现恢复能力。这点设计跟Go是相似的(recover+panic),我觉得挺好。

在实际开发中,异常(错误)处理这方面的痛点还是比较大的。为此我们需要借助第三方库来帮助缓解一下。有个库叫做anyhow,正如其名”any”,它把异常的类型全部抹掉了,这是很不健康的处理方式(熟悉js的同学应该听过 typescript 与 “anyscript” 的梗)。目前我认为更好的处理方式是借助thiserror这个库,它只做了一些基本的辅助工作,没有造成太多破坏。

就代码量来说,Rust处理异常部分的代码完全不会比Golang更少,只不过在Go里都是千篇一律的if err!= nil,而在rust里可以用五花八门(matchif let?等等)的处理方式罢了。

自动化测试

和Go一样,Rust直接在编译工具中内置了单元测试的能力,使用体验基本是一致的,我很轻松地就从Go的思维切换过来。

集成测试的能力挺好的,不过从技术上来说,仅仅是在代码文件位置上做了约定,其实并没有提供什么特殊的能力。

并发

书中提到了Go语言的理念:『Do not communicate by sharing memory; instead, share memory by communicating.』。总体来说,这本书(或者说Rust核心团队)对Go语言在并发方面的理念是认可的。

Rust语言也给出了类似于Go的并发解决方案:提供了类似chan能力的东西,例如std::sync::mpsc

不过在协程这块,Rust并没有提供像go一样方便的、语言内置的工具,Rust本身只是非常保守地提供了并发所需的基本能力,至于要使用多线程还是协程则由用户自行决定,并只能去互联网中搜寻热门的库来使用。提供协程能力的比较热门的库有:tokio, async-std, 和官方团队维护的futures-rs.

面向对象

Rust没有典型的面向对象的能力,与Go通过组合来模拟的思路类似地,Rust使用Trait附带默认实现函数的方式,也实现了有限的面向对象能力。

lib与bin与tests

Rust(或者说cargo)强硬地规定了代码目录的组织方式,包括:

最多只能有一个lib,可以有多个bin:这点确实是一个非常好的习惯,我在接触Rust之前,在尝试用Golang来写命令行工具的时候就遇到了类似的困惑,然后我自己琢磨出的结果也是与现在Rust所规定的方式是相似的。

作为一个反面例子,我觉得可以看看drone-plugins / drone-slack这个库。它做为DroneCI的一个插件,负责的工作有2个:处理来自DroneCI的环境变量、将结果转化为slack消息并发送出去。当我尝试在这个库的基础上再开发一个针对飞书的插件的时候,我懵逼了,我本来只想调用它的”处理来自DroneCI的环境变量”这个部分的逻辑代码,但是由于它的代码全部写在package main里面,导致我无法从外面引用,进而导致我无法用import的方式去引用他的代码,而只能fork出来直接改源代码。

Rust是鼓励把test的代码与源代码写在同一个文件里的,这样相关的代码可以更加耦合,相比Go来说我觉得稍微更舒服一些些,当然Go的测试系统本身我觉得已经足够好用了。

编译目标

作为一门”贴近底层”的语言,Rust理所当然地被期望能够在嵌入式、wasm、cuda等”贴近底层”的运行环境下发挥作用。

然而,据我的了解,除了在wasm方面稍微有值得一提的成绩之外,其他的领域都是残缺不齐的。强烈建议现在不要轻易进这个坑!!请绕行!!

文档资料

Rust在这一点上做得还是挺好的,包括《The Rust Programming Language》在内的多个教程文档写得十分地详尽(甚至让我觉得有些啰嗦)。但是,尽管文档写得详尽,但由于语言本身的难度以及不完善,依然会导致”跟着文档做一切正常,自己动手就两眼抹黑”的难受局面。

从我个人感受为例,在学习Rust阶段,遇到的问题太多太多,每个小坑都要去搜索相关答案,而Rust相关的问答和博客文章是肉眼可见的远远少于Go、TS/JS等语言的,时不时就会遇到无法解决的大坑。

难受的点

use满天飞,有的时候看代码不知道是从哪个库引用过来的。

分号!!分号!!不写分号等同于return,这种万物皆是值的语法特性,我认为“语句”这是C语言遗留下来的糟粕,Go丢弃了,而Rust保留了。

不写return的最大的问题倒还不在于读起来难受,而在于IDE的类型推断会被误导。在写代码的过程中,我们肯定要先写函数签名,这样就明确了返回值类型,于是接下来的大部分写代码时间中,我们一直都在写“最后一行”代码,因此语言服务器不断地将你当前正在写的代码的类型推断为函数值返回类型,导致IDE很多提示功能失效、并且产生大量无意义的语法报错(一直持续到你补上一个分号)。

说到IDE能力(包括语言服务器能力),目前看来,Rust的语言能力还处在很弱很弱的阶段(相当于go语言的早期阶段),在 Clion 中,类型推断、变量用法等能力都还是很残缺的,更别说花里胡哨的宏了,很多问题都要等到cargo(或者rustc)编译时才能暴露出来,大大增加了开发的时间和痛苦。至于 VSCode 也是半斤八两,之前主流的插件叫『Rust』已经被明确标记为deprecated,现在主推的插件叫做『rust-analyzer』还处于零点几版本的初步阶段,它在语言服务器本身的实现上好像稍微优秀一些,但是VSCode编辑器本身的素质还是与Jetbrains差太多了。

(可能是因为 webstorm 实在是太强了吧,我写其他语言的时候多多少少都会觉得难受。在我的眼中,一门语言“好不好”,其中一项不可或缺的评价标准是『IDE的支持程度』,它会极大地影响最终日常开发的实际体验)

此外还有一个不可忽视的巨大问题:Rust的轮子(开源库)的质量非常糟糕!

我在尝试《The Rust Programming Language》第20章的作业——实现一个多线程版本的web框架——的时候,在教程示例的基础上,我想自己动手写一个稍微更加完善一些的web框架,基于我原先看过的gin等仓库的源码,要设计一个简单的web框架的话我脑子里的思路是很清晰的。然而一上手我就懵逼了:就为了寻找一个HTTP协议的解析和封装的库,我花了8个小时的时间,尝试过httphttpcodec两个库,都以失败告终,就这么简单的、第一步的小需求居然就难倒了我一个多年多语言全栈工程师(以及背后跟我一起讨论方案的小伙伴们),Rust这门语言的生态真的是一言难尽。

发展前景:Linux采用Rust

参考阅读:《Linux 核心採納 Rust 的狀況》

简而言之,Rust的采用现状可以总结为以下三点:

  • 不会用Rust重构C
  • 尝试用Rust写新的代码
  • 使两种语言所写代码更好地结合

我认为,大概就像 k8s(及云原生)对于Go的意义那样,Linux对于Rust也将会产生极大的证明和推动作用。Linux如此重量级的项目,随着其中的Rust代码越多,势必会有越来越多的人开始重视这门新语言(其中可能以C/C++程序员为典型代表),然后开源项目越来越多,逐渐形成生态。

对于个人来说,Rust同样值得拥有。拿它来写业务(也就是所谓的functional programming)肯定是不太合适的,怎么想都觉得蛋疼;但是当偶尔遇到系统级开发的需求的时候(所谓的system programming),它将会是一把趁手的武器——换句话说,你可能别无选择,否则你去试试C/C++?

不过,就现状而言,Rust应该说还是远远比不上 c/cpp 的,原因还是在生态太薄弱了(而生态薄弱的原因之一又是学习曲线过于陡峭——学得人少——生态更惨淡如此恶性循环)。就我目前所收集到的信息来看,Rust更多还是一种在有追求的程序员群体中用来尝鲜的东西,还远远达不到可以直接用来做大型商业项目的程度。只能说未来可期吧。

总结:学习新语言时,我在学什么?

作为一个已经熟练掌握Go语言的全栈工程师,我对Rust的期待是:它可以帮助我处理一些原本只能由c/c++来达成的特殊需求(嵌入式、wasm、cuda等)。

Go语言的初衷是想要打造一个『better c』。虽然Go的抽象已经很少了,性能已经很接近c系语言了,但是由于它的GC的存在,它一直不被很多人所认可,而且Go语言在上述的那些特殊领域中的表现也不算太好,因此目前的我依然需要一种强力的工具来作为补充。

从这个角度来说,Rust也许会是合格『better C(++)』,而Go可能不得不承认自己只是『better Python/Java/php』。

在我的期待中,TS用来写终端/大前端业务(业务多变且要求开发速度),Go用来写后台业务(在稳定、性能、开发速度中取得理想的平衡),最后再由Rust补齐“底层”以及“特殊用途”的需求之后,至此,我的技能版图就完整了。

可是,Rust的实际表现让我很失望。同样的学习难度,屎得各有风味,那我为什么不回去学c++呢?c++的应用价值明显是比Rust强大太多的,至少在可见的未来之内这个趋势都不会被改变。

冷静下来想一想,就如小伙伴所说:”学Rust最大的作用不是为了用它,而是理解一种新的编程理念,让我们以后写c++的时候能更规范、更少BUG“。确实,不同的语言、框架之间是可以相互启发的,例如,我在尝试c#之后开始在TS中也实践面向对象、在熟悉TS的类型系统之后对Go的泛型系统有了不同的认识、在熟悉Go的协程之后对js的并发模型理解更顺畅等等……这也是我学习新技术最看重的地方。从这个角度来说,Rust的学习是有很大意义的,它让我对内存管理有了新的理解。

未来可期,再观望观望吧。