普通视图

发现新文章,点击刷新页面。
昨天以前无辄的栈

二十六岁,长期主义与追星逐月

2025年11月26日 08:00

窗外是南半球盛夏的灿烂阳光,微凉的穿堂风透过纱窗,独对着屏幕与半杯咖啡,此时此刻,我用思考与写作来庆祝生日。

一半是懒惰,一半是逃避,去年的我用「写在人生首马一个月之后」替代了一年一度的生日博文(甚至发表日期都在生日的几周之外),所以今年的这一篇,目光会远望过去的两年,尝试记录下我在这段时间的所思所感。

大约在一周前,我参加了神户马拉松并成功 PB;大约在一个半月前,我从前司离职并开始了闲散的失业生活。跑步与工作,占据了我(离职前)生活的大部分,也是我反复思考的课题。

跑步

在 2024 年的悉尼马拉松之后,我这一年的训练与比赛总体上算是令人满意。我参加了世界各地的 5 场马拉松与 1 场半程马拉松,也一次次地提升了我的马拉松最好成绩 —— 从首马的 4 小时 20 分 到最近神户马拉松的 3 小时 04 分,其间有许多值得记录的经历与思考,但这篇文章毕竟不是我的跑步日志,所以摘取其中我印象最为深刻的一些。

2024 年新加坡马拉松

悉尼马拉松赛后,因为膝盖的伤痛,我并没有太充分的训练,甚至可以说是严重地训练不足,但可惜当时的我对马拉松不够敬畏,反而抱着太多侥幸(毕竟我已经完赛过一次了)。

为了尽可能避开热带的阳光与高温,新加坡马拉松起跑时间在凌晨四点半。匆匆洗漱出门,在忐忑与兴奋中,我还是站在了起跑线后。

2024 SCSM

不出所料地,尽管我放低了预期与配速,但还是在三十多公里之后,双脚愈发疼痛而难以为继,只能在一步一步的煎熬中,伴着渐渐升高的太阳,慢慢挪向终点。这最后的十多公里,在无所遮蔽而没有观众的高速公路路段,我的痛苦来源于烈日的暴晒与身体的疼痛;而在有志愿者与观众欢呼的 CBD 路段,我想要努力跑起来,却无法抗拒身体的告警,只能苦笑挥手,低头前行。

我不记得我是如何抵达终点,甚至连完赛时间也记不分明,但我至今依旧可以回忆起那一天的沮丧与无力。

2025 年武汉马拉松

这是我第一场「完整跑下来」的马拉松。

在新加坡马拉松的失利后,我有些不确定自己是否有能力完一场马拉松 —— 或许在 42 公里的某一个点,我的身体(也许是膝盖,也许是脚踝,也许是某个我从未预料会有问题的部位)就会迫使我不得不以走代跑。同时,新加坡日复一日的炎热与潮湿,也让我的日常训练不甚轻松:哪怕是训练计划中的「轻松跑」,哪怕我在天亮之前开跑,也常常会越跑越艰难,心率渐渐攀升,体感与轻松分道扬镳。所以,虽然我一直在严格遵守训练计划,但心里始终没有底,不清楚自己究竟跑向何方。

赛前的几天,在到达武汉之后,见到久违的春天与多年未见的好友,我试探性地在解放公园轻松慢跑,没有看手表上的配速与心率,只觉得格外地愉悦,突然有了许多信心。

解放公园

赛前的一晚,我对着电脑,思量计算着预期的完赛时间与配速。佳明手表预计我的马拉松最好成绩会是 3 小时 45 分左右。相较于悉尼马拉松的成绩,这已然是不小的提升,而且我平时也鲜有跑到这样的速度。习惯性地把这个问题抛给 ChatGPT,在一番对谈之后,它也认为 3:45 是一个合理的目标。然而,回想着自己这几个月的持之以恒,感受着武汉相对怡人的天气,内心深处的一个声音有所不甘:或许,我应该目标更加激进一些。反复权衡,既担心 30 公里后的未知状况,又希望可以跑出自己的最好状态,最终决定用「负分割」(即,前半程留有余力,后半程全力以赴,配速渐渐加快)的战略去面对次日的比赛。

有一种说法,一场完美的马拉松应当由三部分组成:30 公里左右前的游刃有余,30 - 35 公里左右的愈发艰难,以及最后几公里的竭尽所能「顶住」。以此概述我的武汉马拉松,恰如其分。

起跑之后,我按照计划中的配速稳定巡航,心无旁骛地感受着身体的反馈,如今回想起来,沿途的风景与路旁围观群众的呐喊是我对武汉马拉松的背景记忆。抵达半程,计时器上的时间告诉我一切尚在掌握,同时身体的疲劳程度依旧可控,所以,按照赛前的打算,我决定渐渐加速。一路到 30 公里之后,疲惫渐渐累积,但心算时间,我对 3:45 内完已然有了十足的信心,心情也越来越放松。

可我还想要更多,我想要尽我所能去追星逐月。我继续加速,同时边跑边与身旁的跑者交流 —— 探问对方的目标与状态,直到找到了一位目标是 3:30 的大哥,他告诉我需要加到的配速,也愿意带我跟着他一路跑到终点。剩下的里程,体感愈发艰难,逐渐逼近快要不行了的边缘,我目光一直锁定着这位陌生跑友的背影,靠着他的带领与鼓劲,我咬着牙,有意识地与无意识地,想着这几个月训练的各种碎片,闷热但瑰丽的日出印象,热带雨中的树丛摇曳,Rail Corridor 散步的野鸡与松鼠,晚餐的羽衣甘蓝沙拉与蓝莓酸奶碗…… 终点前一两公里,我还是跟丢了这位陌生跑友,所以有些遗憾,没有办法在结束后向他道谢。但不管怎样,我快速瞟了一眼手表上的时间与距离,漱最后一口的运动饮料,就这样在 3:29 冲到了终点线。

武汉马拉松奖牌

难以用言语描述我在终点的感受。是全部力量耗尽连完整的句子也说不出的绝对疲惫,是紧绷了 42 公里的弦终于松开的如释重负,也是仿佛自己完成了一件天大的成就的狂喜。缓缓挪着双腿,随着人群一步一步向前,有些麻木地接过志愿者递上的奖牌与赛后补给,听着一声一声的「恭喜完赛」,又好像觉得不太真确 —— 我真的跑完了武汉马拉松?我真的达成了 3:29 的 PB?最后的最后,人流的尽头,接驳车把大家都带走。龇牙咧嘴地抬起酸痛的腿上车,或拎或背着这一袋赛后物资,各有各的下一站。

黄金海岸、悉尼、神户

在黄金海岸与神户,依旧是全力以赴,依旧是追星逐月地从 3:29 到 3:13,再从 3:13 到 3:04。这两场比赛,在对成绩的追求上和武汉有些相似,但是又有许多值得记述的独特回忆,或许以后会有更合适的分享时机。

黄金海岸马拉松参赛照片

黄金海岸马拉松奖牌

神户马拉松 - 日本马拉松诞生地

神户马拉松奖牌

而在悉尼,一年之后故地重游。赛前并没有任何的成绩目标,只是计划了轻松随意的配速,最终 3:32 完赛。相比一年前的首马,心态上如同一场郊游,体感上也轻松数倍,所以,这真真切切地证明了:我这一年的训练让我有了很大的提升。

悉尼马拉松参赛照片

悉尼马拉松奖牌

训练与思考

其实训练本身不过是日复一日的重复与缓慢提升。回看昨天的训练,上周的训练,甚至上个月的训练,都很难觉察出明显的不同;但不知不觉中,一年过去,比照一年前的自己,每次训练的距离和配速都显著地提升了。

或许所有的变化都发生在不经意的间隙中,我需要做的,不过是坚持跑下去。这一年,不论是出差还是旅行,我的行李箱里始终有一双跑鞋,也尽量严格地遵守了我的训练计划。在某个突然意识到自己进步的瞬间,我在 Threads 上写道

从准备第一场马拉松开始,陆陆续续训练了大半年,期间各种小伤小病而时不时停跑,但大体还算是坚持下来。一直觉得自己跑得没啥进步,六分配跑超过半小时后心率就渐渐失控往上涨,绝对速度也快不起来。当然这有新加坡的天气因素,但正如大家说,强者不抱怨环境,新加坡也有跑步很厉害的人。所以对自我的认知一向不敢自信,只能说「一直还在跑着」。


但上周的一次跑步,第一次跑到 Rail Corridor,在下过雨的清晨湿润中,跑着和往常一样的配速,出乎意料地感觉到了「轻松」,以及越跑越舒爽的跑者愉悦 runner’s high。在那一瞬间,怀疑了手表 GPS,怀疑了自己的感受,才犹豫着接受了这突然而来的进步。那一天发生了什么呢?除了在一个新的路线上跑步,一切都不过如常。


在接下来的一个礼拜,我换了不同的跑步路线,也尝试了轻松跑、渐加速跑,但惊喜的是,这种「轻松」的感觉一直没有消退,RQ 的即时跑力也确认了我的进步。


我不理解这突然的进步,但我很开心。

值得一提的是,这一年我关注了不少跑步相关的博主。他们大部分时候是我闲暇的陪伴,但我也常常因为他们的观点和行动而深受触动。直接摘录我今年 5 月份在 Threads 上的一条推文

膝盖不适,几乎没法跑步的一周,反而思考了更多关于跑步的事情。

跑步的某些时刻是需要一点点信念感的,比如受伤停跑的沮丧,又比如马拉松最后的几公里。

基普乔格在破二努力中说出的那句话,No human is limited,曾经一度给我很多力量,但仿佛又始终有一些不太真实的疏离感:我一个业余跑者,怎么会没有上限呢?我没法跑那么快,也没法跑那么多。

后来,无意中发现了一位精英跑者的频道(philybowden),从她那里学到了一种新的态度:Love the grind。自然,比赛日的狂欢,个人极限的突破,这些高光时刻让人振奋。但更多的时候,跑步就是日复一日的「磨」,在外人(以及某些时刻的自己)看来,相当琐碎无聊,还要时不时面对伤病的困扰。可是,要想长久的跑下去,就需要学会接受、享受这种日常。

又想到我认可的其他跑步博主。黑影儿TV,作为相对接近普通人的跑者,风雨无阻地跑了这么多年,成绩一点一点地进步,我觉得这或许是我将来的一种可能性。山雨小月在关于波士顿马拉松的视频中提到,波马是对跑者多年努力的一种奖励,就像餐后的甜点一样(仅凭记忆复述)。我希望我能有站在波马起点的那一天。

写完才想起来,其实还有一位跑者(世杰Ski)的态度我也觉得很有道理,「只争朝夕的长期主义者」,尤其是他最近关于成绩和伤病的思考。

现在我的想法大体上依旧,额外的是,在这条推文之后,偶然听到了「信口开合」播客采访李犁教练的一期节目《无伤跑到老:金牌教练的长期主义》,加之上文提到的黑影儿TV对其的推崇与个人解读,我也渐渐理解并相信了跑步的「长期主义」。

从我认真跑步开始,其实也不过一两年,所以于我而言,一切不过是长期跑步的开端。想到这,既觉得安心从容,又对自己充满了信心。

工作

作为一个普通的上班族,无可避免的,工作占据了睡眠时间之外的绝大部分。

如果直面内心,去年的我之所以跳过了生日博文,其中的主要原因大概是对工作的不知从何表述。而在此刻,从前司离开一个半月有余,或许站在一定的距离之外,可以更加平静地来回顾这两年多的工作。

在两年前的生日博文中,我其实已经描述了工作的内容与环境,以及我的大体感受。简单概括的话,这是一份量化交易公司的岗位,但是并不直接与交易打交道,而是相当标准的软件工程岗位。工作环境与文化非常好,绝大多数同事都有优秀的背景和能力,日常的工作沟通友好而高效。在工作时间上可以做到很好的 WLB,不用担心加班,同时薪酬也高于市场水平。我使用一门小众的函数式语言 OCaml,维护内部的一些工具,组内的其他同事大多位于全球的其他办公室。

基于这样的描述,我似乎找不到任何离开的理由,但是在过去的几个月,我对工作渐渐失去了热情,直至最终无以为继。

我反复思考其中的原因:有一些其实是工作无关的个人事务,有一些是工作中过于具体而不便公开讨论的事项(不论是签署的保密协议还是简单的职业素养),剩下的一些理由或许听上去有些大而空泛,但确是我真实的感受。

价值感

我曾经一度相信:工作本身不需要有意义,只要能 WLB 且挣到足够的薪水,在工作之外做喜欢的事情追求意义即可。

我很难说这种观念是否正确,但在现在的我看来,它至少并不适用于所有人,也不 100% 适用于我。

在一家金融公司做内部系统,一切都在公司的高墙之内。从微观来看,其实这和维护其他系统(比如开源项目,比如大公司 APP)并没有太大的差别,日常面对的也无非是具体的任务和一行行的代码,也有各自的技术挑战;但如果从其中抽离,以一个非程序员的外部视角,审视自己所做的一切,便慢慢感觉到了局限 —— 我工作的好与坏,只会影响到少量的内部用户和公司的营收,当然,还有我的年终奖。后面两个数字的变化可大可小,但它们只会偶尔(一年一度地)出现在我的面前,很难让我直观上建立起不论正面或负面的反馈。

这一点或许是所有大公司程序员所面对的共同处境:身处一个庞大的机器中,自己所掌控的不过是一个螺丝、一把扳手。但相较于科技行业,金融行业整体更加封闭,似乎每一家公司都在自己的堡垒内悄悄建设着什么宏伟而精密的秘密武器,也许是闭门造车,也许是掌握了独家秘笈,但总之是为了在与彼此的军备竞赛中占据上风。这是一个零和游戏吗?在科技行业如日中天的那些年,雄心勃勃的创业者常说自己所写的代码可以「改变世界」。这或许有些夸大其辞,但如果自己所做的事至少可以影响到公司之外的一些人,会不会有一种「被看见」的意义感呢?

当然,比较公平地说,在我工作的绝大部分时间,我并不会因此感到困扰 —— 前司在华尔街金融公司中已然是最注重工程师文化的一家(至少是「之一」),而且我也十分认可前司大多数同事的技术能力和工程实践,工作体验其实相当不错(我相信优于不少科技公司的组)。可是,当我因为其他原因的叠加而对工作产生倦怠的苗头时,价值感的缺失让我难以找到意义的锚点,从而越漂越远,直至看不见这艘船。

巧合的是,最近看到了 laike9m 的一篇推文,虽然和我的出发点截然不同,但似乎抵达了相似的心境。

职业发展

也许价值感有些过于虚无缥缈,那么具体一些,职业发展也是我反复思考而没有满意答案的问题,尤其是在这个 LLM 大有「替代程序员」的势头的时代。

在两年前的生日博文中,我写道:

但从公司整体的技术栈来说,和互联网公司追求(或者发明)时新技术的思路不同,我司在很多方面依旧使用着相当「传统」的技术,比如,没有使用 Docker/Kubernetes 之类的云原生方案,而是部署到具体的物理机或虚拟机。但在一开始的惊讶和不适应之后,我也渐渐理解了这些背后的原因。我相信,不论使用何种框架或者工具,作为软件工程师,最核心的技术能力都是相通的。

我依旧相信软件工程师的核心能力不是对某一门语言、某一项技术的熟悉程度,而且在前司的这两年半有余,不骄不馁,我自认为我的许多能力也确实得到了显著的提升,比如人际沟通与处理项目的复杂度,所以我并不遗憾。但是一直使用一门极其冷门小众的语言(OCaml),几乎只与内部的系统和项目打交道,我也时不时会觉得有些隐隐地不安 —— 我是不是「点歪了科技树」?如果我现在需要去找一份新的工作,我有多大的信心可以找到令我满意的?抛开公司的光环与平台杠杆,作为一个软件工程师,我现在能做成什么?

我不知道。实话说,这些焦虑在我入职的新鲜劲过了之后其实就若隐若现了,我也好几次和公司内较为资深的同事探讨(包括在加入公司前的面试问答中),没有人能给出完全令人信服的答案,但每个人似乎又都能找到一些很有道理的解释 —— 譬如上面引文中我自己的解释。

而 LLM 时代的开启,又让这个问题更加难以回答一些了。假如 LLM 可以写好每一门(流行的)语言,用好每一个框架,那对于一个人类软件工程师而言,似乎对语言和框架的熟识也没有那么重要了,反而应该尽量发挥自身更为抽象更为高层的能力,或许是系统设计,或许是与其他人类的沟通,又或许是 LLM 尚未学到的 domain knowledge 和难以用文字传述的 knowhow。就我自身经验而言,我对现代前端只有入门水平,但是在简单学会使用 LLM 和相关工具之后,我便可以做出之前难以想象的前端页面,比如记录我马拉松参赛的 run.zackwu.com

既然如此,我的焦虑似乎应该减少?

也许是 FoMo,也许是面对一个黑盒的不安,也许是更深层次的存在危机,也许只是纯粹是想要理解自己所用的工具,我时不时会焦虑于 LLM 的日新月异。成为一个精通 LLM 的使用的软件工程师,或许并不是我想象中的职业发展。

又回到现实的考量。市场上为数不多依旧在招聘的公司,大多数也依旧对于软件工程师有着具体的期许和技术匹配要求,有些甚至在岗位名称上都会冠以某某语言工程师。所以回到我一开始自问的几个问题,我有信心在一定的准备之后找到新的工作,但是如果直接面对当下的求职市场,我并不能轻松地自我证明我的能力和价值。

在我内心深处,有时候有这样一条模糊的逻辑:如果我不会在这家公司一辈子工作下去,如果在这家公司的工作并不会让我在寻求下一份工作更有竞争力,甚至某种意义上限制了我的竞争力,也许待得越久越难以做出离开的决定。

职业生涯很长……

我一直都知道并相信,职业生涯很长,我现在不过是刚过起点。但在日复一日的工作与生活之间,诚实面对自己,怀疑的念头又时不时会浮起。

如同跑步一样,虽然我暂时还不知道自己最终会跑到怎样的水平,但我选择说服自己相信长期主义。我离开前司或许是一个明智的选择,或许是一个将来会后悔的错误,但至少现在的我是解脱而放松的,在长远的职业生涯中,我容许自己犯错误和休息。

至于下一步何去何从,那便是需要我去认真思考的下一个问题(也欢迎读到这里的朋友们指点)。长期主义的另一面,是下定决心之后的追星逐月。

最后

在开始写这篇文章的时候,脑中只有几个非常模糊的关键词:跑步,工作,长期主义,追星逐月。从晨跑后写到日落,其间穿插着几场汹涌的偶阵雨和我时不时的「摸鱼」,最终也零零碎碎写了这么长(其实我早该写一写其中的一些内容了,而不是留到今天)。

我对许多问题还没有想清楚,或许还在怀疑,或许还在摇摆,又或许在错误的方向一路狂奔,但毕竟二十六年我出生在这一天,所以就当这是一次对不成熟观点的 coredump 吧,留待之后的文章引用评述。

初学前端,制作一个 Gatsby 静态博客

2020年9月28日 08:00

背景

大约一个月之前,作为一个几乎没有任何前端基础的小白(仅仅手写过简单的 HTML + CSS 页面),在一直从事的后端开发领域之外,突然对「现代化」前端产生了浓烈的好奇与兴趣:毕竟,我学计算机的初衷是创造美好的事物,总是和命令行打交道,不由得觉得有所欠缺。

另一方面,虽然靠着魔改现有的 Jekyll 主题,这几年我陆陆续续给我的博客进行了多次改造,但其实我一直有股彻底重写、制作一个完全符合自己审美的博客的冲动。伴着 Ruby 的式微,Jekyll 虽然背靠 Github,但在近年迭出的静态博客框架(如 Hugo、Hexo、Gatsby 等)中,也不免有些陈旧过时,这更是让我萌生了用新的框架制作博客的想法。

一个月前的推文

而在近一个月的知识学习与代码编写之后,我成功用上了全新的个人博客,也对现代化前端有了一定的认识,所以写这篇文章,作为记录。

前端入门

在开始制作博客之前,不论选用何种框架与技术,首先需要学习基本的前端知识。

在请教了一些前端开发者之后,结合我的实际需求,我大致的学习路径如下:

  1. HTML 与 CSS
  2. 布局与设计
  3. React
  4. 其他现代化前端概念

其中多数内容的学习是在 freeCodeCamp 完成。该网站上的课程免费开放,且每一小节均带有在线的交互式练习,学习过程中具有明确的即时反馈,在此我强烈推荐给想要学习前端的同学。

HTML 与 CSS

由于在之前的项目中,我写过一些简单的传统前端,这一部分对我来说没有太大的困难。

跟随着 freeCodeCamp 的课程,我快速地过了一遍常用的 HTML 标签与一些新的语义化标签,并对 accessibility 有了一定的认知。(不要写出无脑嵌套 div 的网页!)

而关于 CSS,最大的困难在于如何给网页布局,这曾经是让我感到无比困扰的难题,所幸随着前端的发展,有了灵活好用的 flexgrid 布局,学习这些新的 CSS 布局方式,让我有一种如释重负的感觉。

布局与设计

其实,这一点更多是审美和艺术,而非前端技术。

作为一个未曾学习过 UI 设计的开发者,我对如何设计出优雅美观的页面尚无太多想法,只有一些朦胧的感受:比如色彩要协调、排版要对齐、风格要统一等。此外,在上面提到的 freeCodeCamp 课程中,对此有一些简单的论述,提到了一些基本的设计原则。

作为一个快速的解决方案,我选择向优秀的设计学习。在这一个月,我刻意观察了一些美观舒适的页面,既留心他们的配色、排版等设计,也研究他们的布局实现方式。而在我最终完成的博客中,也一定程度借鉴了其他人的设计。

React

前端框架之争,我入门之前就有所耳闻。在此我选择了 React 而非 Vue,原因主要在于我的项目没有历史包袱,完全是从零实现,加上 Gatsby 静态博客的绑定。

React 很复杂,但是我的任务并不复杂:静态博客几乎不涉及状态管理,所以我只学习了十分基础的 React,便足以完成开发。

其他现代化前端概念

比起传统的 HTML,CSS,JavaScript 三件套,现代化前端有层出不穷的新奇框架与工具,作为一个初学者,其中有一些我觉得十分有趣且实用:

  • SCSS/Sass:对 CSS 做出了许多有用且符合直觉的拓展,譬如嵌套的样式定义。我并没有特地去学习,而是在掌握基本的 CSS 之后,凭借直觉和搜索引擎开始编写 SCSS,其间没有遇到太大的障碍。

  • Styled Components:比起传统的全局样式,在 React 中,直接编写组件级别的样式更为简单快捷,同时也能减少不同组件之间的样式干扰。

  • Node.js 与 NPM:通过 NPM,前端项目也具有了包管理,能够方便地添加和移除依赖,让后端开发的我感到十分舒适。

  • ……

博客编写

为什么是 Gatsby

回到最初的静态博客框架技术选型。经过调研,目前比较流行的 静态网站生成器(static site generator) (按照其 Github 项目的 star 数排序)有:

  1. Next.js:功能强大且用途丰富的 React 框架,但并非专门的静态网站生成器,故排除
  2. Gatsby:基于 React,具有完备的插件生态系统,被 React 官网 所使用
  3. Hugo:Go 语言编写,高性能,适用于大型静态网站,使用广泛,如 Kubernetes 官网
  4. Jekyll:Ruby 语言编写,Github Page 对其有着很好的支持,但是近年来流行度有所下降
  5. Hexo:Node.js 语言编写,比较流行
  6. Vuepress:基于 Vue,流行程度不如上述框架,故排除

如果从实用角度出发,我想我的选择是 Hugo,足够强大,足够流行,并且我对 Go 语言足够熟悉,能够快速地上手;然而 Hugo 的缺点在于「不够前端」,整体设计上看,比较偏向使用传统的前端写法(HTML 模板加上全局 CSS),没有用到现代化前端的强大功能。

因此,考虑到除了完成开发之外,我还希望能够学习现代化的前端开发,最终的选择便是 Gatsby:根植于前端生态(仿照 cloud native 的说法,或许是 modern frontend native?),流行而强大。

另一方面,根据 Wappalyzer 的数据,Gatsby 确实是近年来最为流行的选择:

Static site generator market share

如何写一个 Gatsby 项目

在此,我无意复述那些 Gatsby 文档中已有的内容,而是仅列出我所用到的一些有用资源。

如果想要从零开始学习了解 Gatsby,我自己采用并推荐的方式是快速过一遍 Gatsby.js Tutorials,便能基本了解 Gatsby 项目的结构与原理(一点都不复杂);如果有比较好的前端基础,也可以直接浏览其官方的 Blog Starter 代码,获得直观的认识。

然后再对照某个实际的 Gatsby 项目,比如 Gatsby Starter Lumen,学习许多实用的细节与技巧;当然,这一步也可以按需进行,比如在不太确定某个做法的最佳实践时,参考这些成熟项目的做法(我正是这么做的)。

除此之外,Gatsby 最为人称道之处在于其完善的 插件系统:当你需要实现某个功能时,大概率已经有某个(甚至多个)插件能够帮你做到这件事,那么最好不要自己重复造轮子。事实上,一个 Gatsby 项目的 gatsby-config.js 往往会导入许多有用的插件,比如 RSS Feed、sitemap、SEO 插件等等,既节省了编写代码的负担,也保持了很好的模块化与可配置性。不过值得一提的是,并不是所有插件都有较高的质量,特别是同一个功能有多个插件时,最好还是认真仔细地对比并试用一下,比如 webfont 相关插件。

我的 Gatsby 博客

回到博客本身。

在开始实现之前,我列出了我所期望具有的功能与设计特点。而这些功能基本上最终都得到了实现:

Trello 看板

如前所述,在编写我的 Gatsby 博客时,我借鉴了许多优秀博客的设计,样式上有所参照。不过由于其他博客的实现是传统的 HTML 模板 + 全局 CSS,而我使用 Gatsby + SCSS + Styled Components,我重新写了大部分代码,以遵循 Gatsby 的最佳实践。大致的源码结构如下:

$ tree src -d -L 3
src
├── components
│   ├── gitalk
│   ├── layout
│   │   └── seo
│   ├── main
│   ├── page
│   ├── pagination
│   ├── post
│   ├── postlist
│   ├── sidebar
│   │   ├── copyright
│   │   ├── menu
│   │   ├── sitemeta
│   │   ├── social-links
│   │   └── toc
│   └── tags
├── pages
├── scss
│   └── base
└── templates

而在具体的编码实现过程中,也曾遇到过许多细枝末节的问题,不过并无太多展开叙述的意义,在此不表。

如有兴趣,完整的代码可见 izackwu/gatsby-starter-breeze,以及 在线 Demo

最后

当你看到这篇文章时,我的博客已经换上了刚完成的 Gatsby 博客主题,焕然一新。

而于我而言,通过给自己的博客从头写一个新的 Gatsby 主题,我入门了现代化的前端开发,期间感觉充实而快乐。

如果你也有一定的空闲时间与精力,那么我推荐你也试着开始探索前端的世界,并创造出自己喜欢的东西。不一定要深入探究前端的高深技术,仅仅用前端的皮毛自娱自乐,就能收获足够多的乐趣。

《构建之法》与微软实习面试

2020年6月22日 08:00

前言

关于微软实习面试的体验,我其实在上一篇文章 (2020 年,我寻找暑期实习的经历与经验 中已经有所涉及:

而最后一轮面试,由部门 leader 进行,形式和前两轮差别不是特别大,不过技术的比重比较低,相当大一部分内容是「谈人生」(即技术之外的价值观与理念).

当时对此一带而过,主要出于不愿过多泄露面试细节的考虑,以及自己当时其实对这场面试尚且有些「摸不着头脑」。

而今旧事重提,完全缘于最近所阅读的 《构建之法》——一本由微软亚洲研究院首席研发经理 邹欣 所写的「软件工程」书籍。在阅读这本书之后,感触颇多,联想不久前参加的微软实习面试:当时觉得其中的面试经历颇为不可思议,如今看来,却似乎可以慢慢领会其中深意。

那么,在这篇文章中,就先以不透露过多细节的方式,尽量较为具体地展开我微软实习的第三轮面试,然后谈谈《构建之法》这本书和我的一些想法。

微软实习的第三轮面试

在微软实习的面试中,我所经历的,前两轮是较为常规的技术面试,第三轮是比较特别的部门 leader 面试。如前所述,这一轮面试技术方面的含量比较低,而技术之外的内容占据了绝大部分。

面试伊始,面试官给了一道相当简单的算法题目:大概只花了 10 分钟不到,我便写出了复杂度上最优的解法。而在我写出解法之后,面试官让我详细解释了每个变量存在的必要性,并反复让我优化代码的写法以提升代码可读性和精简程度。

至此,仅仅 20 分钟不到。

然而,让我没想到的是,在整场面试之后的剩余时间,足足 1 个小时有余,我经历了极为不可思议的「灵魂拷问」。围绕着一个核心问题:「这样做就够了吗?什么时候可以确定你任务完成了?」,面试官抛出了一个又一个问题,我则战战兢兢地尝试着做出一个又一个回答。比如:

  • 如何为这个程序设计测试?
  • 你所设计的这些测试真的足够了吗?
  • 对于一个函数,你实现了它,那么怎样可以确保你的任务完成了,什么时候你可以放心地说「我可以下班了」呢?
  • 测试的本质是什么呢?你测试的目的是什么呢?
  • 测试的目的是确保程序的预期和实际相符吗?
  • 有了完美的测试覆盖率,就能确保预期和实际相符吗?
  • 写程序的目的到底是什么呢?

一切的一切,这些连环「拷问」,在面试临近结束的最后 10 分钟,终于真相大白:

  • 面试官:你刚才提到了预期,那比预期的程度还要强一点的是?
  • 我:要求?
  • 面试官:正确,谁的要求?
  • 我:客户的要求?
  • 面试官:完全正确!我们微软之所以能做出比别家更好的软件,成为成功的企业,就是因为我们满足了客户的要求。在很多时候,你写了一个软件,但是并没有搞清楚客户的要求,所以会失败。在微软,其实你要花 99% 的时间去思考,然后才能动手写代码。

原话不完全如此,但大意如此。

就我个人的感受而言,当我听到「客户的要求」时,一时觉得十分难以接受,甚至产生了严重的怀疑:问了这么多问题,结果最终的答案居然是这个?这场面试的意义何在?是不是因为我之前的表现,所以这场面试只是走个过场?……

幸运的是,尽管我在这场面试中表现只能算差强人意,但最终还是成功通过了。然而,这场面试,在之后很长的一段时间困扰着我,直到最近我读了《构建之法》这本书。

《构建之法》与软件工程

坦白来说,软件工程是我在大学期间唯一的一门没有好好学的计算机专业课程。

一方面是学校的课程质量低下。学校的这门课开设于大二,所用的教材是 《面对对象软件工程》, 一本厚厚的陈旧的传统教材。而当时的授课老师,上课也不过是复述着书上的各种概念,就连举例子也生搬硬套书上给出的那些明显不符合我国社会环境的例子。一学期听下来,只觉得大而无当,云里雾里,除了让我认识了各种图表和文档,在当时看来觉得几乎毫无收获,因此便划水飘过。

另一方面是我对软件工程意义的不了解与轻视。当时的我,没有经历过大型项目的开发,也没有真正开发过有商业价值的软件系统,仅仅是写了许多玩具级别的项目,自然是意识不到软件工程的意义。更何况,即便通过学校开设的课程,所做的课程项目,除了应付交差,也完全用不上所学的各种繁复的图表与文档,更是给我留下了对软件工程的不佳印象。

那么,为什么最近我会主动去阅读《构建之法》来学习软件工程呢?其实很简单,上述两个方面的困顿随着我的学习与成长而渐渐消散了,所以学习软件工程是一件很自然的事情。在实习过程中接触了业界真正创造商业价值的软件系统,在 Github 上参与了较为大型的开源项目,以及学校后续课程项目与他人真正地协作开发(而不是「抱大腿式的小组项目」)的经历,均使得我渐渐意识到:不应该用「手工作坊式」的方法进行开发,而需要学习成熟的开发流程。而《构建之法》这本书,其实在学习软件工程课程时,身边的同学便已经向我推荐过。

那么,《构建之法》这本书讲了些什么?又好在哪里呢?

其实一言以概之,《构建之法》这本书以符合实际(包括业界开发的实际和大学软件工程教学的实际)的方式,有侧重地讲解了真正有意义的软件工程实践,而对于传统教材着墨颇多的图表和文档点到辄止,让人能够愉快且带着兴趣地读下去并有所收获。顺便一提,这本书的英文名是 Build to win,也很好地概括了书的内容。

在这里我无意摘抄书中的章句,只是简单谈谈我阅读后的收获与体会。

三个公式

书中让我印象最为深刻的,是三个简单的公式:

  1. 程序 = 算法 + 数据结构
  2. 软件 = 程序 + 软件工程
  3. 软件企业 = 软件 + 商业模式

在刚接触计算机时,许多人往往沉醉于第一个公式,学习各种精巧的算法与数据结构,能够用很巧妙的方式写出程序。其实我很佩服这样的人,尤其是在高中阶段便接触 OI 竞赛学习算法的同学,他们的程序设计能力远远超过我。

而在学习了许多基础概念之后,我便开始了在各种玩具项目中编写代码,这一阶段并不能意识到软件工程的重要性,反而会觉得软件工程碍手碍脚(我都能一个晚上开发出来,还需要写文档设计架构做什么呢?),正如我学习软件工程这门课的时候。而通过接触大型的、商业的软件项目,与越来越多的人协作开发,便会很自然地意识到第二个公式的意义。

至于第三个公式,却往往被过分强调以贬低第二个公式。作者在书中时不时的提及我国新兴的互联网企业:他们 996 高负荷工作却缺乏成熟的软件工程能力,也能依靠创新的商业模式取得了巨大的成功。(当然,书中也不是全然贬低这些互联网企业,关于敏捷开发,这些企业应当是相当优秀的。)

回想微软实习的第三轮面试,最终的答案是「客户的需求」,当时或许觉得有些难以理解,现在对照这三个公式,不也合情合理吗?从商业公司(比如微软)的角度去看待,程序设计的终极目的是满足客户的需求,那么对于开发人员,什么时候才能确定自己任务完成呢?自然是客户的需求被满足之后。虽然在面试中这个问题或许不甚妥当(毕竟答案过于唯一且出乎意料),但确实对我有很大的启发,让我开始从不同的角度去审视自己的技能与职业发展,思考实际中工作的意义与价值。

于我而言,目前的层次是努力接近第二个公式,因而学习软件工程。

现代软件工程

学校授课所选定的教材,其陈腐的一角,体现在对现代软件工程的敏捷开发毫无介绍,而是采用了传统的瀑布流和文档式开发。而这本书,则介绍了较为符合当下业界实际的各种开发流程。

作者作为微软(或许在不少人眼中已然陈旧落伍的企业)的员工,其写的内容是对二者的平衡:既保留了传统软件工程的优秀实践,也引进了现代软件工程的敏捷与效率。比如持续集成(CI)与持续交付(CD),现在已经被广泛应用,就连我的 Github 玩具项目也开始使用,而陈腐的教材对此避而不谈;又比如测试驱动开发(TDD)、结对编程等各种开发模式,也是对于软件开发流程的颠覆与创新,赋予了不同开发场景下的多种实践方式。

事实上,现代软件工程的实践繁多,但这种繁多不同于陈腐教材中文档与图表的繁多。前者是有价值的字典般的繁多,可以根据自身的实际情况合理选择以达到精简干练;后者往往是流程的繁多,为了追求某种「绝对的仪式感」而削足适履创造出的冗杂(当然,需要澄清的是,在特定场合下,这样的繁复流程还是有价值的)。

书中介绍了许多或时髦或传统的开发模式。模式虽多,我在阅读时大体却只是快速浏览,仅仅关注于那些我已经遇到过对应场景的开发模式,读起来颇有种得心应手的舒畅与事后总结般的灵光一现。

测试之道

什么是测试呢?在传统的软件工程教材中,给出了测试所需的一系列文档和图表,以及分门别类的各种测试类型;而在本书中,作者也给出了多种测试,并且提出了测试的一些要求和规范(不是以文档的形式!)。

不过有趣的是,书中强调测试人员的重要性,甚至举了某公司裁撤测试人员作为反面例子,然而微软却在前些年大刀阔斧地砍掉了测试团队,让开发人员负责测试。

那么话说回来,测试到底是为了什么呢?在面试中,我和面试官就此达成了共识:测试是为了确保程序的实际和预期相符,包括功能性和非功能性。而为了达到这个目的,我们有各种各样的测试手段,比如单元测试、集成测试等等。但还是回到终极答案上,我们的终极目的是什么呢?是满足客户的需求,测试不过是满足「我们假想出来的客户需求」。

总结

在即将开始微软的暑期实习之前,飞快地读完《构建之法》之后,回想几个月前的微软面试经历,胡思乱想了很多,所以写了这篇文章。

其实,作为一个大三学生,我对软件工程的理解还是相当肤浅的:虽然有了一定的开发经历,但不过是浅尝辄止。有没有什么方法可以学习软件工程呢?或许阅读《构建之法》是一个不错的方法,但纸上得来终觉浅,我所缺乏的,是更多的实际开发经历。

然而不论如何,这一切都只是思考的起点与探索的开始,写出来和大家分享。

2020 年,我寻找暑期实习的经历与经验

2020年4月23日 08:00

概述

2020 年,大三,按照常规的人生节奏,我踏上了寻找暑期实习的征途。

先开门见山一下,在写此文时,我基本结束了寻找暑期实习的历程。总的来说,我一共投递了 5 家公司(谷歌,微软,亚马逊,腾讯,蚂蚁金服),并最终选择了微软。具体情况是:

  • 亚马逊,可能是因为投的太晚了而直接石沉大海,杳无音信
  • 谷歌,面试进行到一半,突然宣布今年因为疫情取消了国内所有的暑期实习
  • 腾讯,成功拿到了 WXG 微信事业群,微信支付部门,后台开发的 offer
  • 蚂蚁金服,顺利拿到了广告技术部门,后台开发的 offer
  • 微软,顺利拿到了上海 Cloud + AI 部门 SWE 实习 offer

经历

需要说明的是,在开始寻找暑期实习之前,我已经在字节跳动有过三个月的日常实习经历。由于这段经历并不是特别愉快(具体见 我之前的文章),我对于即将到来的暑期实习,我所期待的,是去体验不同于国内互联网企业的另一种工作与生活方式。

那么,在寻找暑期实习伊始,我仅仅选定了两家目标公司:谷歌与微软。而可能出于外企较为慵懒的工作效率以及我简历的不够过硬,在我网申投出简历两三个月后,这两家公司依旧毫无进展。自然而然地,在两三个月无果后,为了减少内心的焦虑不安,我稍做准备,额外挑了两家国内互联网企业:腾讯与蚂蚁金服,以期「保底」。因此,总体时间线如下图所示:

Timeline

再逐公司记录一下我寻找实习的经历吧。

谷歌

我最早投递的,即是谷歌。在去年 12 月,谷歌放出今年的暑期实习岗位时,我就立刻网申投递简历(此处其实犯了一个很严重的错误,后文详述);一个月后,谷歌的招聘人员联系了我,填写了一些诸如期望城市、方便的面试时间等额外信息,等待安排面试。然而,虽然每周五 HR 会给我发一封进度跟进的邮件,但自此长达两个月的面试等待期实在让人难以忍受。

终于,在三月中旬,漫长的等待后,我迎来了谷歌的第一场电话面试。如网上许多人的描述,谷歌的面试可以说是:开局一个空白的 Google Doc 文档,面试不聊别的,全靠做算法题。于我而言,遇到的面试官很友善,沟通很不错,而且题目难度也不高(两道题,最终都完美做出来了),总体体验比较愉快。因此,第一场面试顺利通过。

然而不巧的是,就在等待第二场面试时,由于新冠疫情的影响,谷歌取消了今年国内所有的暑期实习,所以我与谷歌也自此暂别。当然,客观来说,从我被安排面试的时间来看,即便没有疫情,大概率也会因为 Team Match 过晚而无法成功拿到谷歌的实习 offer,故坦然处之。

微软

紧随谷歌之后,在今年的 1 月,微软放出今年的暑期实习岗位时,我依旧立刻网申投递了简历(此处所犯错误同上)。然而幸运的是,在 2 月末,亡羊补牢,我赶上了微软内推的截止期,由一位学长帮助进行了内推。

微软的实习面试安排在 3 月底,就我所面试的部门(上海,Cloud + AI)而言,三轮面试安排在了一个下午,相当紧凑。第一轮面试与第二轮面试类似,都是由一位 Individual Contributor 进行技术面试,其中涉及到简历上内容的介绍、计算机基础知识以及常规的算法题,总体难度不大,面试十分友好,体验十分愉快。而最后一轮面试,由部门 leader 进行,形式和前两轮差别不是特别大,不过技术的比重比较低,相当大一部门内容是「谈人生」(即技术之外的价值观与理念)。

有趣的是,我前两轮的面试官,均为女性工程师(大概是我今年寻找暑期实习过程中所遇到的,仅有的二位女性工程师);而第三轮的面试官,似乎是一个中文讲得很好的外国人(不完全确定,通过口音判断),而且还放了我一次鸽子,不过总的来说,微软的三轮面试体验还是相当不错的,没有太大的压力。

而在面试结束后,大致两周(官方说法)内告知面试结果。根据为数的不多样本,似乎如果面试没有通过,拒信发得很快;而如果面试顺利通过,等待期可能会长达一个月。

在我的多次邮件催促下,终于熬过漫长的等待,于 4 月中旬,顺利收到了 offer,并选择接受。

腾讯

如前所述,腾讯并非我一开始的目标公司,而是在等待的焦虑中为了「保底」而投递。在 3 月中旬,等待谷歌和面试时,我联系到了某位腾讯 HR,由她帮助,内推投递了暑期实习。

不得不夸赞的是,腾讯(包括后文的蚂蚁金服),处理实习申请的流程极为迅速,短短 20 天内,便完成了「两次面试流程」:第一次面试 TEG 事业群,在两轮面试之后失败告终;第二次面试 WXG 事业群,在三轮面试后成功拿到 offer。

其实,倘若顺利的话,腾讯实习,应当是只有三轮面试的:前两轮技术面试,第三轮 HR 面试。此外,如果一个部门面试失败,会有机会被其他部门「重新捞起」,从头开始面试流程。

至于面试经历,我所经历的四轮技术面试,大同小异,无非是询问一些计算机基本知识和简历上内容,而且巧合的是,四轮面试都未遇到算法题。而 HR 面试,主要是沟通一下入职事项,了解个人性格等等,基本上不会在这一轮面试失败。

值得一谈的是,我在前两轮面试的「作死行为」。第一轮面试,面试官询问我是否参与过开源项目,我答没有,他对此略有不满。在面试的最后,由我提问,我问面试官「贵部门应该是使用了许多开源项目,那么,是否有回馈开源社区?」,面试官一时语塞;第二轮面试,不知为何,聊到工作与生活的平衡,我吐槽 996 工作制的不合理与反人性,面试官沉默不语,而我果不其然地在面试结束后几小时内收到了面试失败的通知。

以此为戒,在第二次面试流程,即,面试 WXG 的过程,总体比较顺利。有些尴尬的是,HR 面试中,HR 似乎是看到了之前面试官对我的评价,一阵见血地询问「你似乎对 996 很有意见,那你是否愿意加班呢?」一时颇为尴尬,所幸最终无恙,成功拿到了微信支付后台开发的 offer。

蚂蚁金服

其实,在申请蚂蚁金服的实习之前,一直十分犹豫于其繁复的面试流程。不过事实表明,蚂蚁金服虽然流程繁琐,但是处理效率很高,从投递到 offer,只花了 20 天不到。

在投递蚂蚁金服之后,首先要做的,是冗长繁琐的行为测试题,包括语文阅读、数学图表阅读、类智力测试题的找规律题以及性格测试题。虽然从结果看,这一部分似乎影响不大,但是实在让人极度不愉快。

不同于其他公司(比如微软、腾讯),蚂蚁金服即便是内推,也无法免去笔试题目。所谓笔试,是在投递后,自选一场笔试考试时间,在牛客平台,完成两道算法编程题目。我所遇到的笔试题目,难度并不大,因而全部顺利做出。

在笔试结束后,即开始面试,共有 5 轮:除了最后一轮是 HR 面试,前四轮都是技术面试,不同点在于面试官,分别是普通员工、小 leader、本部门 leader、其他部门 leader。前四轮技术面试,总体上和腾讯的技术面试差不多,包括简历内容、计算机基本知识、算法题以及系统设计题,总体难度不大,不过偶有我回答不出的题目。而最后一轮 HR 面试,据传是有一定概率在此失败的(阿里的 HR 似乎权力比较大?)。

在面试中,我发现了比较有意思的一个现象:即便是技术面,所遇到的面试官的口才也相当不错,能够就我的提问,滔滔不绝且逻辑清晰地回答十余分钟而无法打断。或许,这是阿里的企业文化所造就?

面试结束,大约两三天后,收到了部门 leader 的电话,告知已获得 offer,不过需要确认我是否有意向接受,若不打算接受,对方为了节省「宝贵的」名额,就不发放正式的 offer。由于此时我已经正式接受了微软的实习 offer,虽然可以多诳骗一个 offer,但是为了不浪费对方的名额,还是如实告知了我的意向。

经验

总的来说,其实我寻找暑期实习的过程还算顺利,虽有些磕绊,但比较幸运,最终还是拿到了不少 offer。

现在回顾这段经历,其实可以发现,我犯了好几个明显的错误:

  1. 微软和谷歌一开始直接网申,而没有走内推通道
  2. 一开始投的公司过少
  3. 面试中过于言而无忌

第 1 点,从我寻找实习的经历来看,内推往往具有极其强大的力量,仅仅靠网申,即便再早投递,处理的优先级也不如内推。倘若我没有亡羊补牢地补上了微软内推,那恐怕至今我离微软 offer 还遥遥无期。所以,在今后的求职中,不论是实习、校招还是社招,尽量让认识的人帮忙内推。

第 2 点,如果不是特别自信,不要把鸡蛋放在一个篮子里,而应该广撒网。多投几家公司。或许,这并不是真的打算去这些公司,而是为了减轻内心的焦虑与不安。在等待中,倘若手头有保底的选项,即便暂时不是最满意的,也能极大地缓解压力。而且事实上,现在自我评估能力,我想,我的能力还不足以自信到只投一两家公司就能稳拿 offer。毕竟,面试中存在相当大的运气成分,「饱和式」申请方能让自己避免意外。

第 3 点,面试虽然是双方互相交流,但是毕竟是一个相对正式的沟通,所以应该把握说话的方式与分寸。坦诚固然是不错的品质,但有所保留或许会给自己多留一点余地。

总结一下,从我寻找暑期实习的经历,我得出的经验可以概括为:内推,广撒网,合理沟通。

在 Github 上使用 GPG 的全过程

2019年8月4日 08:00

起因

其实在很早之前 Github 就已经充分支持 GPG 密钥了,而在我之前使用 Github 的两年时间内,竟对此一无所知,实在有些“没见过世面”。直至近日,在一次偶然查看仓库的 commit 历史中,发现某些 commit 有一个不同寻常的绿色标记(Verified),不仅美观(?)而且看上去舒心,如图所示:

漂亮的 Verified 标记

点击这个标记,得知这一次 commit 是经过签名验证的(signed with a verified signature),因此,我便开始研究如何利用 GPG 对自己的每次 commit 进行签名验证。

什么是 GPG

GnuPG is a complete and free implementation of the OpenPGP standard as defined by RFC4880(also known as PGP). GnuPG allows you to encrypt and sign your data and communications; it features a versatile key management system, along with access modules for all kinds of public key directories. GnuPG, also known as GPG, is a command line tool with features for easy integration with other applications.

以上是从 GPG 网站 上摘取的部分简介,总的来说,GPG 的功能十分丰富,然而我这次主要是用它来对 Git 中的 commit 进行签名验证,所以需要做的事情也不算太复杂:

  1. 生成自己的 GPG 密钥
  2. 关联 GPG 公钥与 Github 账户
  3. 设置利用 GPG 私钥对 commit 进行签名
  4. 可选步骤:信任 Github 的 GPG 密钥

过程

安装 GPG

由于我的目的是在 Git 中使用 GPG,而 Windows 版本的 Git 发行包中,已经包含了可用的 GPG 命令行。判断方法也很简单,打开 Git Bash,输入gpg --version,可以看到类似的 GPG 版本信息:

$ gpg --version
gpg (GnuPG) 2.2.16-unknown
libgcrypt 1.8.4
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Home: /c/Users/---/.gnupg
Supported algorithms:
Pubkey: RSA, ELG, DSA, ECDH, ECDSA, EDDSA
Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,
        CAMELLIA128, CAMELLIA192, CAMELLIA256
Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB, BZIP2

不过需要说明的是,如果所安装的 Git 版本比较久远(比如我一开始所用的 Git 发行包是 2017 年的),那么很可能其包含的 GPG 版本过低,影响后续的操作,所以建议直接更新 Git 发行包至最新版本。

生成自己的 GPG 密钥

打开 Git Bash,运行gpg --full-generate-key,根据提示,输入相应的个人信息(需要注意的是邮箱必须要使用在 Github 中验证过的邮箱)、自定义密钥参数、设置私钥密码等等,即可生成自己的 GPG 密钥。(补充说明,使用gpg --gen-key亦可生成密钥,但是会略去自定义密钥参数的步骤,对于一般场合的使用倒也问题不大。)

输出结果的末尾大致如下:

gpg: key DC3DB5873563E6B2 marked as ultimately trusted
gpg: revocation certificate stored as '/c/Users/---/.gnupg/openpgp-revocs.d/1BA074F113915706D141348CDC3DB5873563E6B2.rev'
public and secret key created and signed.

pub   rsa2048 2019-08-04 [SC] [expires: 2021-08-03]
      1BA074F113915706D141348CDC3DB5873563E6B2
uid                      fortest <test@test.com>
sub   rsa2048 2019-08-04 [E] [expires: 2021-08-03]

需要记下的,是上述输出信息中的密钥 ID:1BA074F113915706D141348CDC3DB5873563E6B2 或者DC3DB5873563E6B2,后者是前者的简短形式。

当然,如果没有及时将其记下也不要紧,可以运行gpg --list-keys,列出本地存储的所有 GPG 密钥信息,大致如下:

$ gpg --list-keys
# some output is omitted here
pub   rsa2048 2019-08-04 [SC] [expires: 2021-08-03]
      1BA074F113915706D141348CDC3DB5873563E6B2
uid           [ultimate] fortest <test@test.com>
sub   rsa2048 2019-08-04 [E] [expires: 2021-08-03]

稍微解读一下这些结果:

  • pub其后的是该密钥的公钥特征,包括了密钥的参数(加密算法是 rsa,长度为 2048,生成于 2019-08-04,用途是 Signing 和 Certificating,一年之后过期)以及密钥的 ID。
  • uid其后的是生成密钥时所输入的个人信息。
  • sub其后的则是该密钥的子密钥特征,格式和公钥部分大致相同(E 表示用途是 Encrypting)。

关联 GPG 公钥与 Github 账户

还记得在上一步中记下的密钥 ID 吗?现在,我们需要根据这个 ID 来导出对应 GPG 密钥的公钥字符串。继续在 Git Bash 中,运行命令gpg --armor --export {key_id}:

$ gpg --armor --export 1BA074F113915706D141348CDC3DB5873563E6B2
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBF1GT3wBCADC9Hb3HtDc69XzYlohVKvdL1KnK0FslJISRuF6S0sdoOiWo2wJ
OiYVplWguTSkrMytjnMsoysZVolkYluY1wk67NT8YuYfnu6LSuF/doihrRldnKmz
9NZWw+15MLnENKsWCtwNwcCGDeZNJACyyUMYk7nJeIiM72k3/rnsyEpHqB25W/Zf
1VBkwf/ShePZ2W+rUktJ8j1TZuxe2bQpJdHQ9EKWG50D8O3xk+N+xEg4pcXLMfwT
vnVpf2wINGLA6+3ypVMDipC0fgAnINBrrjiKsq2Sskv0O73D3sZlkOi0jgAhx+21
5dI2xHbcs3DrcZbWAF1xEA8wGsoyYQWoSCBrABEBAAG0F2ZvcnRlc3QgPHRlc3RA
dGVzdC5jb20+iQFUBBMBCAA+FiEEG6B08RORVwbRQTSM3D21hzVj5rIFAl1GT3wC
GwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ3D21hzVj5rJ3BQgA
nUusNKaf8SIWq1w4ZR6CKhZP+kz+5kOEBs3+qIXJV++9nbjs4jnqOnXJUUdpLS9E
HGYnd6XSeyqWmBAuFCcmld4VGIajYxgDbF11/ql5Gnbu26/jV7hnrBBK6Xn/6oV9
bBmLoT9xget5xFC6g2VE0EvneRqacUgMBCkvrMzcVnHmpkSOpjfXRAItnyK/bhia
8k/+5URO8v7Ao2+QO0zk8XzgGc5B8H/yItzDiKe7gpzdUyCviG8m/tkDUURzloY4
09wCmQWWzerbBHJT4RdpPqdTEtC6f4jTuT32zp5NtLpJ740WmSJly/8nAJ/0x3Vf
pVkzhsg9gVHe/JSFa6/hXbkBDQRdRk98AQgAyjXZ98VOgftRThuGuYxKhqahonLf
Ihu+NuNMFG6sGGzkm2T+1i4uKyM8T/kGdcTzTXE/SMHmrCMz94FNcQ77/OFLz5HY
8hjaz5Sun7iNmz5HGct8OrsP6gQeJ5ucqm3vDZmnwU/+J+wcTosv5mgWoBVob7jb
PBnoNVBQSVhD2ek4CDljn3PdReqYfe+ee8yn+6K1t9c1HHHMco3WpdgofUABd+7l
Q1LF8IpBRDvWgdMciAPaSthIqFT6R6xLQhXV8SUm0mr2/GXbYqIptjvy1JmUwNk3
jE3LOLYulZChRdvVg3Y+xgkVlMYLy3SBQ1EaTnUUsGYbhGQnOwDwVAlxgwARAQAB
iQE8BBgBCAAmFiEEG6B08RORVwbRQTSM3D21hzVj5rIFAl1GT3wCGwwFCQPCZwAA
CgkQ3D21hzVj5rLzvAf/QzfDOrhRz9AVLiAqus3Z/WfZY81sUiewNM+YdV9aODht
q4VE92SYHeR/b72+Fl62SRbDqxw7qG5FJGByuqo6nJjHEpnFzqB/pepTVDzlwvdn
JO46tmepFAChPBpeTTjTs2CF/BG0As0KxXQCpdFw4m8UdkZ7Olt1/LKnXrFmr1BA
jp2MvmAo38j2RyPTyXKWmJW+vC8DwmOGMoHCL6fM0TeaWey3rNxST7bbxPdRVc4Z
/26k450FEW5D+VInb9NuFYSoE2UXs6DgI1OWuuGvWePrtXHeQvuNbGdEdUwU14mf
msQ78G2MjX4AAYR5iNnQ/IWDBKbOWt3ajIoJuebArw==
=oHpZ
-----END PGP PUBLIC KEY BLOCK-----

然后,在 Github 的 SSH and GPG keys 中,新增一个 GPG key,内容即是上述命令的输出结果。

再次提醒,GPG 密钥中个人信息的邮箱部分,必须使用在 Github 中验证过的邮箱,否则添加 GPG key 会提示未经验证。

利用 GPG 私钥对 Git commit 进行签名

首先,需要让 Git 知道签名所用的 GPG 密钥 ID:

git config --global user.signingkey {key_id}

然后,在每次 commit 的时候,加上-S参数,表示这次提交需要用 GPG 密钥进行签名:

git commit -S -m "..."

如果觉得每次都需要手动加上-S有些麻烦,可以设置 Git 为每次 commit 自动要求签名:

git config --global commit.gpgsign true

但不论是否需要手动加上-S,在 commit 时皆会弹出对话框,需要输入该密钥的密码,以确保是密钥拥有者本人操作,如图所示:

GPG Signing on commit

输入正确密码后,本次 commit 便被签名验证,push 到 Github 远程仓库后,即可显示出 Verified 绿色标记(由于fortest <test@test.com>密钥的邮箱未经验证,所以此处实际用的是我本人的密钥进行签名):

结果

可选步骤:信任 Github 的 GPG 密钥

事实上,在完成上述步骤后,已经可以基本完全正常地同时使用 Github 和 GPG 了,那为什么还需要这一步骤呢?很简单,不妨用git log --show-signature试试查看本地的某个 Git 仓库的 commit 记录和签名信息:

$ git log --show-signature
# some output is omitted
commit ec37d4af120a69dafa077052cfdf4f5e33fa1ef3 (HEAD -> master)
gpg: Signature made 2019 年 08 月 412:52:29
gpg:                using RSA key 1BA074F113915706D141348CDC3DB5873563E6B2
gpg: Good signature from "fortest <test@test.com>" [ultimate]
Author: keithnull <keith1126@126.com>
Date:   Sun Aug 4 12:52:29 2019 +0800

    test GPG

commit 6937d638d950362f73bfbf28bc4a39d1700bf26b
gpg: Signature made 2019 年 07 月 2415:58:46
gpg:                using RSA key 4AEE18F83AFDEB23
gpg: Can't check signature: No public key
Author: Keith Null <20233656+keithnull@users.noreply.github.com>
Date:   Wed Jul 24 15:58:46 2019 +0800

    Initial commit

可以发现,虽然所有的 commit 在 Github 中查看都是 Verified,但是有一些比较特殊:在 Github 网页端进行的操作,比如创建仓库。这些 commit 并没有用我们之前生成的密钥进行签名,而是由 Github 代为签名了。这样的结果就是,我们本地无法确认这些签名的真实性。

为了解决这个问题,我们需要导入并信任 Github 所用的 GPG 密钥

先是导入:

$ curl https://github.com/web-flow.gpg | gpg --import
# curl's output is omitted
gpg: key 4AEE18F83AFDEB23: public key "GitHub (web-flow commit signing) <noreply@github.com>" imported
gpg: Total number processed: 1
gpg:               imported: 1

然后是信任(用自己的密钥为其签名验证,需要输入密码):

$ gpg --sign-key 4AEE18F83AFDEB23
pub  rsa2048/4AEE18F83AFDEB23
     created: 2017-08-16  expires: never       usage: SC
     trust: unknown       validity: full
[  full  ] (1). GitHub (web-flow commit signing) <noreply@github.com>

pub  rsa2048/4AEE18F83AFDEB23
     created: 2017-08-16  expires: never       usage: SC
     trust: unknown       validity: full
 Primary key fingerprint: 5DE3 E050 9C47 EA3C F04A  42D3 4AEE 18F8 3AFD EB23

     GitHub (web-flow commit signing) <noreply@github.com>

Are you sure that you want to sign this key with your
key "Keith Null <keith1126@126.com>" (7C4BC917F7B12E8A)

Really sign? (y/N) y

至此,再尝试查看本地仓库的 commit 签名信息,则会发现所有的 commit 签名都已得到验证:

$ git log --show-signature
# some output is omitted
commit 6937d638d950362f73bfbf28bc4a39d1700bf26b
gpg: Signature made 2019 年 07 月 2415:58:46
gpg:                using RSA key 4AEE18F83AFDEB23
gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [full]
Author: Keith Null <20233656+keithnull@users.noreply.github.com>
Date:   Wed Jul 24 15:58:46 2019 +0800

    Initial commit

结束

经过这一番操作,Github 和 GPG 圆满结合在了一起,而我也得到了我想要的 Verified 标记。不过,GPG 的功能远非止于此,它还可以用来对文件、邮件等进行加密,还可以进行身份验证等等,都有待我去学习研究。

❌
❌