HQ Software Foundation

我的编程技艺

April 10, 2023

写下这篇文章,是为了给未来一个启示。学习编程已经有了六年时间,如果我真的没有足够的热情,绝对坚持不到今天。之前几个月我经历了一段非常艰难的时光,但我仍然不知道未来该怎么做。

知识的积累需要归纳整理经验并建立体系,技艺需要不断的练习来取得进步,而最难的,是发现事情的本质。我一直想尽办法试图发现一些本质上的东西,无论是在本科、公司还是研究生阶段,但几乎所有的尝试都以失败告终。说实在的,我还能够对编程保持热情也是一个奇迹。

这里简单回顾下我做过和想做的所有计算机相关的项目。从中可以大致找到我在编程技艺上探索的过程。如果说一点收获没有,那确实不至于,但是我从来没做出让我感到有成就感的项目,这种对自己的厌恶一直持续到现在。

项目 时间 类型 编程语言 备注
打飞机游戏 2017/4 游戏 Python pygame
坦克大战游戏 2017/5 游戏 C++ 计算机实践,使用 VC6.0
电梯控制系统 2018/3 硬件 Verilog 基于FPGA
流水线芯片 2018/4 硬件 Verilog 计算机组成原理 课程设计
认识实习 2018/8 Web Java Spring、JSP、SSM框架
图片分类 2018/11 AI Python 基于Keras的RNN
出勤系统 2019/4 Web Python、JavaScript 软件工程 课程设计,使用Flask和Vue,GItHub看板,多人项目
ShadowHTTP 2019/5 网络 Python 计算机网络 课程设计,asyncio
生产实习 2019/7 Web Python Docker部署,使用Django,Jenkins持续集成
KPL-API 2020/7 C++ 公司的第一个项目
KPL 2020/8 Web Go Kubernetes,重构
KPL-IOT 2020/9 Web Go、JavaScript WebTTY、gRPC、Redis
lcbot 2021/7 插件 Python Slack API、DynamoDB
CalorieTracker 2021/9 应用 Java 安卓应用
TFA 2021/11 Web TypeScript 全栈
easyview-VR 2022/5 Web JavaScript A-Frame、3D
easyview-wasm 2022/7 C++ WebAssembly、IndexedDB、WebWorker
渲染器 2022/9 图形 JavaScript WebGL
Platform Game 2022/10 游戏 C++ SFML、ZeroMQ、V8
CodeAnalysis 2023/1 插件 TypeScript VSCode、ANTLR、LSP

这些基本上是我真正做完了的项目的一个总结吧。

接触编程是从C++开始,在学校的课程上用Dev C++这类的IDE写简单的小程序。后来在学校的一个网站做题,只涉及一些C++语法的知识点和最简单的数据结构算法。 我当时很喜欢读C++ Primer,也照着书上写了一些简单的东西,但是写一个完整的项目还是很困难的,尤其是如何配置环境上。

夏季学期的C++游戏项目虽然是用着被淘汰的VC6.0(代码现在都已经丢失了),但我还是坚定了转专业的决心。暑假我又在学习平台上买了一份课程,总之,学习C++总是让人去着重于这门语言本身——这门语言的特性实在是太多了。理解多重继承、函数决议、模板推导、异常处理、智能指针、右值引用、移动语义、泛型容器这些东西,分开来看可能不难,但是没有应用场景的话,就像无根之水一样容易被忘记。C++这门语言的独特性很难不让人喜欢,我承认我沦陷于此。

相比之下,学习Python的过程倒还算顺利。这门语言相对简单,只要照着教程就能做很多事情。后来,我又读了很久的《流畅的Python》,这是我对优雅代码的第一次直观感受。我把这些经验用在了后面的网络项目,还有Flask和Django两个Web项目上。仅仅是把代码写得足够Pythonic也能让人非常开心。

读《深入理解计算机系统》花了我整整一学期的时间。我并没有把所有的项目做完。这本书里面的流水线设计对后来的Verilog项目帮助极大,拆弹游戏让我面对汇编也能冷静分析。简单看了后面的Shell和Web服务器也都让我理解了Linux的一部分原理,之后我立刻买了《Unix环境高级编程》,但很快被这里面字典般的知识吓怕了。

除去数理基础课,大学时候专业课的一些作业也花费了我不少时间,但这些都不能算完整的项目,因为我们只需要理解系统的一部分就能完成教学目标。比如在操作系统课上研究实验用的系统,在数值计算课上练习Matlab,编译原理课上写LL(1)解析器,网络课上配置交换机仿真,数据库课编写SQL,C#课画窗口,机器学习竞赛用显卡炼丹,等等。我没资格说国内的大学课程过于简单,但平心而论,课上提供的内容的确有限,只能给学生一个大致的方向引导,而没办法培养出足以入门的能力。

针对当时找工作唯一的方向:Web开发,这两次暑期课程对于找工作的帮助比大学所有课加一起还要多。第一次的技术考古学课程里教的都是2000年代的东西,用的也是老掉牙的Eclipse和Java 1.6,但是在图书馆看一些旧书的过程却让我开始理解历史发展的过程。我们的课程从Spring和依赖注入讲起直到MVC架构,似乎有种空中楼阁的感觉,让人过于迷惑。不过第二个夏天,我已经在freeCodeCamp上学过了HTML、CSS和JQuery,写点简单的页面对我来说已经轻而易举。我尝试了当时还比较新潮的Docker和Jenkins等新技术,照着几个网站复刻了个漂亮的页面。可能还读了一些DevOps的书,不过离实际还是有很大距离。

有意思的是,当时第一次去字节面试(2018年),在他们的食堂面试时,面试官问我兴趣是什么,我说我想做中间件。而我当时根本不知道中间件是什么!面试官追问的时我也只能说是消息队列一类的东西。因为下意识的就觉得中间件很高级,很有趣,当时也看了一些开源项目的源码,不出意外,我被深深地打击到,像无头苍蝇一样在代码库中浏览了一段时间就放弃了自己能够看懂某个项目的念头。

最重要的课是软件工程,作为一门必修课,它的影响更加深远。这是我们第一次体验多人共同开发。在这个项目期间,我一直在读《构建之法》。五个人的团队要结合起来开发一个前后端分离的应用程序,从各个角度都不是一件容易的事,而且还要照着书上关于敏捷开发的流程一步步走:从用例图、UML,到需求文档和看板,再到接口文档和前后端实现,这应该是最接近公司的开发模式了。不过,当时基础知识和编程能力的缺乏让这种训练丧失了不少意义,我们花了太多的时间在配置环境和普及常识上。现在回想起来,这里应该还是需要为每个小组配置一个经验丰富的助教,才能有比较大的收获,而不是让几个零基础的同学瞎折腾。不过我们也没什么办法来指责现有的教育体系了。

上面没有写关于函数式编程的项目,因为我一直试图入门,但从未能明白自己该干什么。读《计算机程序的构造和解释》让我对Lisp有了很大的兴趣。当然,要把这本书里面的那个虚拟机搞懂可是不太容易,但这种看待问题的思路真的很让人向往!之后我又找了许许多多的的文章和书籍,比如Haskell,Scheme和Scala的教程。《Scala函数式编程》一直是我春节回家无聊的时候的读物,理解flatMap、foldr和applicative可以视作有趣的思维小游戏。但直到现在,我觉得我似乎理解了Monad,可这对真正做一个项目毫无帮助。我并不知道我能做什么,函数式的编程方式到底在什么情况下适合应用。用函数式语言写Web是否有些奇怪?但我甚至不知道除了Web自己还能写点什么!我知道这意味着抽象程度更深的代码,但首先需要找到一个能够用得上抽象的问题。

我觉得我缺少一些恒心。我应该专一一些,真正做一个属于自己的项目,至少是能够让自己从中获得一点成就感——但到现在也没有过。如果说编程只是调API解决问题,那我至少应该先知道自己能解决什么问题。

也许我应该参与一个开源项目。这并不难——


在大学阶段我的收获有很多。其中一点是,一些工程上的细节,比如配置功能开关、包管理器和IDE虽然对编码并不重要,但是对开发的体验是至关重要的。折腾Linux、VSCode、mingw、Homebrew、Idea、npm之类的环境花了我大量的脑细胞!另一点是,从他人身上学习非常重要。我是一个非常社恐的人,在现实世界中不敢和同学们分享,也没勇气在网上的技术论坛发言。但实际上,编程作为一项技艺,传统的师徒制可能仍然是一个非常有效的方式。至少,在大规模生成式语言模型出现之前,搜索引擎很难回答一些奇怪的问题。

所以毕业后,我还是打算去上班了。虽然没能通过大厂的面试,但小公司也有其优势。我进入了一个比较早应用Go语言和Kubernetes的团队,也在云服务上有了很多积累。而且,我当时想的是在工作之外我还有足够多的时间完成我个人的项目(后面证明这简直是天方夜谭)。

进公司之前,我希望有人能够成为我编程技艺上的导师,但很快我就在心中默默扮演起了公司救世主的角色。即使是刚刚参加工作,我也很快意识到实际用于生产的代码没有想的那么完美。所有人都在忙碌之中,看板上积压了一大堆已知问题,而代码库几乎没有文档。我也试着去修复一些Bug,但也都是通过现象能快速定位的那种。所有人都很忙,我只能从头一个个地去读提交信息,搞明白功能,理清调用路径,画出模块图,并搞明白了很多临时方案的前因后果,我也终于在实践中学会Git的不少高级用法。

这个项目是to B的,所以没什么用户量的压力,大多数情况我们线上版本的并发访问量最高也就在个位数。由于没有卖出去几份,复杂需求也不是很多。这个项目没什么编程规范也能运行得不错。当然,我自己的进步是很大的。我搞明白了ORM的原理,如何设计表之间的关系,解决N+1问题,数据库迁移的流程,还有如何使用索引之类的问题。但是这总是和真正重要的那些技术还差了一些距离。

在公司我可以潜心研究Web框架的源码和协议的细节来修改bug,在家我就看一些关于企业应用架构的书籍,最后我看到了DDD,我觉得这就是我要找的答案。那段时间我读了很多DDD的书籍,却发现公司的项目远远没有复杂到需要考虑聚合根和充血对象的程度。我还看了《架构整洁之道》,我学着书上的方式计算模块之间的依赖度,得出的结论是,我们的代码不该这么复杂。

我自告奋勇重构代码,这件事并没有那么容易:至少对于分层架构而言,跨层的依赖是不应该出现的;一些复制粘贴的代码可以拆分到函数中;初始化数据对象的方式可以更加简洁;没处理的错误应该处理掉。总之,就是这一类的简单工作。Go语言总体上并不是一门严格的语言,这些提升可读性的努力相当有意义。我不知道我是否是太追求代码的优雅性了,但我坚信在代码库膨胀到无法维护之前,这么做是有意义的。

从书上和博客里得到的另一个启示是,引入单元测试是必要的。但最后我也没能搞成这个目标,我们最后只是草草引入了一个接口平台和一个集成测试平台。总之,你很难去用测试替身去模拟所有的依赖,除非在设计接口的时候就想着如何测试它。而很多时候业务需求的突然变更会打破所有的假设条件。有一次我突然发现,很多业务中关于事务的实现完全是错的!整个项目在Service层来回调用,一大堆嵌套的事务我甚至不知道会不会生效,而回滚的逻辑明显也不太对,尤其是涉及RPC的场合,我只能祈祷永远不会碰到需要手动修复数据库的场合!

我讨厌写文档和注释,实际上所有人都讨厌写文档和注释。大部分的Web项目都很无聊,无非是处理一下HTTP协议,校验一下参数,读写一下数据库。所谓的业务不过是一些规则的嵌套,处理一些边界条件,然后就是调用第三方的API。我花了很久才搞明白如何把我们的系统接入OIDC的实现,当然还免不了迁移旧的微服务的杂乱事情。总之,只有折腾才有KPI,我也是明白了为什么中国的程序员都这么累了。

另一方面,这个项目复杂在还需要处理k8s集群的维护工作。我还学到了一些关于如何在服务器(和NFS)上读写、解压文件的麻烦事情。为什么这个东西要设计成这个样子?这里一定有一些哲学在其中,但每天改bug和新功能的开发让我无心思索这些。

冬天我被叫过去搞一个IOT项目。首先是使用WebSocket转发GRPC,然后在后端再用mqtt转发一次到设备,通过Redis做分布式锁和消息队列,最后把终端渲染在网页上。整个流程麻烦但不困难,总之,这些API的保证不免让我感到好奇。我们总是在依赖一些基础设施,而我一直对这些组件的内部毫无概念。也许这是我想要的项目吗?

随着时间发展,项目从我刚来时的30000行逐渐膨胀到将近80000行代码。我知道很多都是冗余的框架填充代码。但是随着交付日的一再延期和层出不穷的Bug,让我意识到我没办法去解决这些问题,无论是地位还是能力都无法推动这些事情的变革,所以我选择了离开。

读了研究生后,从理性角度,我知道自己应该以就业为主。但还是和以前一样,我希望自己能从一个项目中找到学习计算机和生活下去的意义。我写了很多东西,但是,我觉得我越来越丧失了对编程的热情。这种热情应该来源于哪里?慢慢地,我逐渐失去了打开编辑器的勇气。

尤其是我想到那些半途而废的项目:

  • 6.824(2019):一个Go语言写的一致性kv存储的实现。这门课相当有名,但我承认直到很久之后我才明白共识算法到底代表什么。在当时,我连如何入手都不是很清楚。
  • leptjson(2019):C JSON解析器项目。这是一个非常规范的项目,采用纯C99实现、测试驱动开发和防御性编程。我懒得把这个项目做完的主要原因是,我觉得我已经学得够多了。
  • coreutils(2020):无聊时学习Rust看到的项目,用Rust重新实现Unix常用工具。说实话,看上去并不复杂,但命令行工具一点不比其他的软件简单。
  • Zero to Prod Rust Web(2022):Rust的Web API项目,我特意在网上买的这本书的电子版。但我后来觉得这不是我想要的东西。Rust不适合做Web项目而且这本书里的做法太繁琐了。
  • Tiger(2022):上过编译器课程后,我觉得应该写点东西。我想把虎书上的那个OCaml的编译器移植到F#上,但这件事情的工作量远远超出我的想象。
  • Scheme24h(2022):24小时用Haskell编写Scheme解释器。之所以能写的这么快,是因为它用了ParseC这个解析器组合子库。我觉得我必须要搞懂这个库在做什么才能继续写下去。
  • RayTracing1w(2022):一周学会光线追踪。前面还是非常轻松的,但是后面我发现需要太多的数学知识。而且图形学就是如此,学无止境。我觉得还是应该把时间用在课上的项目上。

还有一些完全没做成的项目:

  • h3rpc(2020):作为本科毕设的项目。我打算基于QUIC开发一个RPC协议。我读了很久的RFC才只搞懂网络的一些细节。实现一个RPC框架实际上需要很多工作,最简单的就是简单替代gRPC的网络层,但这也没那么容易做到。
  • DrCCTProf(2022):在这门课上我本来想基于Rust提供的MIR进行一些动态分析。但我到最后也想不明白这两个东西如何联系到一起。最后我只能把几个方法移植到Rust上了事。
  • Fuzzing(2022):另一门课的设计。我本来想做一个基于AST做模糊测试的项目。我们找了一篇论文的代码,生成SQL的模糊测试,跑了20多个小时都没找到SQLite的一个错误。后来发现SQLite自己的测试比功能代码还多得多。

但实际上,这种完全没做成的项目都是因为自己完全不知道自己该做什么。甚至有些东西我很想做点什么,但完全没找到一种可行性!比如,我学了LLVM框架下的一堆概念,但我也不知道能用它搞出什么东西。另一种情况是,我总是想追求更复杂的东西,而导致每次都过于高估自己了。

一些问题是常见的:

  • 能做什么?首先是兴趣在哪里,其次是能做到什么,这是很关键的。有时候一个库,一个新技术,一个新的应用场景就能快速利用起来。所谓的脑洞就是如此,比如新出现了AI技术,人们就会把它推广到很多地方。
  • 什么时候开始做?需要一些基础知识。
  • 预估什么目标?
  • 遇到困难怎么办?
  • 做到什么程度为止?写测试?

从这种角度看,写下一些东西是至关重要的。无论是看书,还是自己从代码中总结到了一些东西。不写下点什么,忘记是极大概率的事件。

我必须提到学校课程对软件项目的影响。

学校会教什么?实际上,最关键的是,学校是需要有一种定期考核的机制,一般是一学期或者半学期。所以必须有一个能够可以考核的教学目标,在一个主题内围绕它展开。那么带来的问题是,我们必须采用自底向下的方式去构建知识体系。所以要先学高数大物,模电数电。对于编程语言,这可能也就足以介绍一些语法。

计算机专业有什么特殊之处吗?我觉得最特殊的是这个学科的实践实在是太过容易,所以导致可以教学的主题以指数级增加。其中相互交错的内容过多,是大学四年完全没有可能按照课程为单位设计教学的。

这就是高等教育和就业市场不匹配的一个核心原因。这些单个的课程之间的衔接要么是毫无用处,要么是高等级的抽象犹如空中楼阁。学习成了行为艺术和糊弄学的集大成。

那么,我觉得更好的方式是什么呢?我们应该去解决一个问题,然后,在这过程中知道自己需要什么知识,然后试着去解决。首先可能是完全没有思路,或者是最直接的做法。再之后,我们发现一些事情可以简化,或者可以从更有效的角度思考问题,这就是学习的一个过程。这和单元测试的思路很相似。

但是问题又来了。你在获取基础知识之前怎么能知道自己能做成什么事情呢?即使你能够做成,又怎么能够知道自己是否适合,或者是能从中得到乐趣呢?不否认有些人确实是能够对某个特定领域产生持续的热情。但是,大学生里绝大多数人也都只有浅尝辄止的兴趣,尤其是在没有引导的情况下。人们不是为了解决问题而来上大学的,大家只不过是为了出人头地,为了金钱财富,或者是简单为了能有一份体面的工作。

另一个问题是,这样对教学资源的要求可能太高了。也许AI现在已经有了能回答所有问题的能力,但是要在教育行业推广还有待实验。

当然,这种培养方式的考核总还是有效的,足以满足社会上企业的简单需求。

实际上,在开放性的课程选题中,我从来没有成功过。我总是花费绝大多数的时间来搞明白自己能做什么不能做什么——这比真正做一件事还要难得多。

Web项目只是无数项目里最不起眼的那一个。对比之下,其实我很认真的看过一些开源项目的代码。当然,大部分也只是看了个开头。

  • xv6(2018):基本操作系统。我当时把代码全都打印出来天天带在包里,看的过程中被多核启动这部分卡住了很久。
  • Redis(2018):4.0版本,最经典的单线程实现。我是看了apue之后希望能够找到一些应用场景的,这里对于epoll的封装确实很漂亮。
  • Git(2018):0.01版本。优雅的设计思路。
  • Shadowsocks(2019):简单的网络代理工具。我大概参考了一些AES加密的内容。
  • 500 Lines(2019):一些小的项目,如果说具体学到了什么可能确实也没有。
  • 4.4BSD(2019):和《TCPIP详解》一起读的网络栈实现的书。这本书有一些平时见不到的C语言用法。
  • leveldb(2021):LSM树引擎

那么读别人的项目其实是一个极大的陷阱,我承认,读代码比写代码难得多。

  • 有何功能?实际上这一步足以挡住99%的人。很多人并不清楚软件功能上的一些细节。对于软件一般有手册页,对于库至少也有一个架构文档。
  • 使用什么库?为什么要用这个库?软件分层和分模块的意义是提供一些不变性,前提是,要能够发现系统中真正不变的东西。对于新语言,要大致知道它的一些基本概念;对于调用库、API、云服务,也要大致知道它能给出的保证。不过,由于你不可能知道所有的细节,我们应该随时准备回到这里,把这个软件和其他系统在概念上分开。
  • 概念如何?作者需要发明一些概念,对软件自身再进一步分层,可能是类,或者是某种结构,某种机制,策略等等。我们可能在阅读项目之前就通过功能对如何实现它有了一些初步的猜测。但作者在内部一定隐藏了一些机制。
  • 架构和组件?对计算的抽象,对数据的抽象。
  • 如何调试?首先是寻找入口点。然后试图观察程序的行为,功能的实现和概念的对应。实际上用调试器是一个很好的方式。相比于用调试器寻找Bug,
  • 如何添加功能?也需要修改依赖包,
  • 用到哪些基础知识?一个软件总是需要用到另一个领域的知识,包括网络、图形、编译,数学,物理,日期时间等等。我们可能需要读一些基础书籍、RFC、论文之类的东西,不过我相信大部分软件都不会需要这些。

那么,关键问题是,我们能够从开源项目中收获什么?

  • 应用的原理:使用一个工具久了,有时候会对其内部的实现产生好奇。好奇心始终是人最大的动力。
  • 模仿:一个成功的开源项目一定有其优点,是值得仔细研究实现的,所谓造轮子的乐趣正是如此。无论是到最后可以写一篇教程,从零开始写一个你自己的Redis之类的。
  • 贡献开源:修改bug、贡献新功能,但这需要研究很长一段时间才能完成。

写了这么多,还是总结一下吧。编程技艺的训练有很多种,无论是个人的兴趣,企业的协作还是开源的贡献都是重要的过程。总结起来,有一些至关重要的因素:

  • 信心:信心从何而来?也许是对自己智商的自信,或者是收集到足够多信息。
  • 专注:在一个项目上持续投入的时间,以及投入时间时的集中程度。

最后我想谈谈关于竞赛型编程的话题,即所谓的“刷题”。关于这点,我必须抛开一直以来的偏见。

我学习编程就是在这种在线测评网站上开始的。这相比传统机械材料行业的金工实习要好得多!但中间有一段时间,这东西深深的打击到我了。我觉得我真的搞不明白这些东西!

尤其是,这逐渐成了面试的标准。虽然从结果上看倒没什么,但这个过程让我很不舒服:我要为了这件事情而去刻意练习,甚至是倒因为果,为了刷题而改变自己的学习方式,这岂不是荒唐!

虽然我一直说我对Leetcode恨之入骨。但实际上我确实也学到了很多东西。CodeWars也是一个很有趣的平台,在这里,很多题目都是用户自己设计出来考别人的。总结一下我们应该从竞赛型编程中学到的东西:

  • 练习集合库的使用,学习语法特性。
  • 常见通用问题的解决方案。这些应该是通过计算思维能独立思考出来。
  • 算法。虽然不会涉及到太多复杂的算法,但是也足以覆盖。

但是相比之下,计算机还有更多有趣的事情可做吧!

(未完待续……)


© 2023, Built on 2023-12-28 11:20 with Gatsby