重构英语:第 8 个月
原文由 Michael Lynch 于 发布,订阅该博客
一句话总结
也许,并非每个人都想加入我的焦点小组。
本月亮点
- 我发现,并非所有购买了抢先体验版的读者都愿意就初稿提供反馈。
- 我梳理了时间的去向,并思考如何减少时间浪费。
- 我花了 10 小时从零重写了一个当初耗时 300 小时搭建的网页应用。
- 我继续用 Gleam 学习函数式编程,但可能有点“作弊”。
目标完成度
每个月初,我都会定下希望完成的目标。以下是本月的完成情况:
与至少 10 位未曾交流过的读者沟通
- 结果:给七位读者发了邮件,收到三封回复,进行了一次实时对话
- 评分:C
坐下来统计之前,我以为回复率要低得多,其实不少读者都有回应。问题在于我联系得还不够多。
我可能在每封邮件上花了太多时间,总想写出独特、明显不是 AI 生成的内容,结果一头扎进对方的博客一看就是一小时。
清理营销创意积压
- 结果:完成了预定计划的约 70%
- 评分:C
这些任务大部分都完成了,不过一年前录的一期访谈还没整理发布,希望能尽快完成。
发布《Refactoring English》的新章节
我对这一章的成稿很满意,但也知道在社交媒体上分享会是一场赌博。结果是在 Lobsters 上反响不错,但在 Hacker News 上毫无水花。
《Refactoring English》数据
| 指标 | 2025 年 6 月 | 2025 年 7 月 | 变化 |
|---|---|---|---|
| 独立访客 | 6,574 | 8,061 | +1,487 (+23%) |
| 预售收入 | $597.24 | $800.04 | +$202.80 (+34%) |
| 咨询收入 | $242.45 | $0.00 | -$242.45 (-100%) |
| 赞助收入 | $48.25 | $48.25 | $0.00 (0%) |
| 总收入 | $887.94 | $848.29 | -$39.65 (-4%) |
网站访问量有所上升,这算是个好消息,毕竟本月并没有特别受欢迎的新文章。我把这看作一个积极的信号:仅靠现有的试读章节,就能维持不错的访问量。
总收入略有下降,只是因为付费咨询从一单降到了零,变化不大。更让我高兴的是,预售收入相比 6 月增长了 34%。
如果大家喜欢的只是感觉上的买书呢?
在我逐一联系读者并与他们视频通话时,有一句话被反复提及:“我还没开始读。”
无论是几天前刚购买的人,还是已经拥有这本书好几个月的人,都会这么说。
我最担心的是,这本书成了“维生素而非止痛药”。人们把好写作看作重要但不紧急的事。有人会在面试前去读一本叫“How to Nail Your Next Coding Interview”的书,但提升写作这种事,却很容易被无限期推迟。
我之所以在动笔前就预售这本书,部分原因就是想看看是否真的有足够多的人愿意为它付费。事实证明是有的,而且至今仍有人在购买,但我担心这个信号或许没有我想象中那么有预测性。
会不会我的书就是那种让人们买来获得一种“已在为写作投资”的感觉、却不必真正花时间去做任何事的东西?会不会它就像 Planet Fitness 那家知名的连锁健身房——据说它的盈利就靠那些办了卡却从不去锻炼的人?

如果我写的是一本“Planet Fitness 式”的书呢?
照片由 Mike Mozart 拍摄,依据 CC-BY-2.0 许可使用
就算我真能成为书里的 Planet Fitness,那也不可持续。要让这本书在财务上可行,我指望的是读过并喜欢它的人的口碑推荐。我希望受欢迎的博主们会引用我的书,说它是帮助他们提升写作的资源。当有开发者说想提高写作水平时,我希望大家会第一时间推荐我的书。
也许顾客并不想加入我的焦点小组
我预售这本书的另一个原因是,我以为预购的读者会特别积极地在写作过程中提供反馈。
和同样是顾客的朋友们聊过之后,大多数人都说,他们预购只是为了支持这个项目,想等书完成后再读,并不一定想参与焦点小组或对初稿提意见。他们只想读最终成品,这完全可以理解。
我应该继续逐一联系大家
通过逐一联系顾客并安排实时通话,我发现有些读者非常热情,很愿意提供反馈。虽然目前只找到了少数几位,但他们给出的反馈都非常宝贵。
找到这些热情读者最有效的方式就是一封一封地发邮件,所以我会继续这么做。
时间都去哪了?
大约每年左右,我都会回顾上个月的成果,然后想:“等等,上个月怎么就只做了这么点?时间都花哪了?”
七月就是这样的一个月。我按正常的全职时长工作了,但回过头来看,却在想:“怎么一个月就只完成了一个新章节?”
所以,来回顾一下,在没完成月度目标的时间里,我的时间都花在了哪里。
在章节上投入过度
几个月前,我意识到自己花了太多时间雕琢文字,而不是在章节达到 80-90 分时就交给读者。我的解决办法是为每章设定时间上限,在预算内能写多少就写多少。
七月写的邮件那一章,我的预算是五小时,实际却花了 17.5 小时。部分原因是刻意为之——设定目标后我意识到,这一章很适合作为网站上的独立试读章节。而先写成试读、再整合回书里,需要花更长时间。
但我现在写的这一章也在犯同样的毛病。已经花了 6.5 小时,超过了 6 小时的预算,估计至少还要 3 小时才能达到可以分享给读者的程度。我想这既是我打磨得太多,也是因为我给这本书最重要、也是第一章的预算定得太低了。
解决办法:为会作为公开试读的章节额外预留时间,并严格将写作时间控制在既定范围内。
课外的博客文章
我喜欢在刚学到东西时就通过写博客记录下来,但就算每周花 20 小时写博客,也写不完所有想写的题材。
我的一些博客文章能吸引可能成为书的读者的那类人,而另一些则可能不会。我必须有意识地权衡,在“纯粹出于兴趣”的文章上投入多少时间。
上个月,我发布了一篇新博文《将 ZFS 存储池从 RAIDZ1 迁移到 RAIDZ2》。我知道它对书没什么帮助,但当时以为只需几小时就能写完,而且想把一个还没见人好好讲清楚的东西解释清楚。
实际上,那篇 ZFS 文章花了我七小时,对于一篇“兴趣使然”的文章来说投入过大了,尤其是在当月没有更能吸引眼球的《Refactoring English》章节可分享的情况下。
解决办法:对非图书相关的博文更挑剔一些,优先写那些也能吸引潜在图书读者的主题。
不良的社交媒体习惯
我发现自己一感到无聊或对艰深的写作、编程提不起劲时,就会去刷社交媒体。我告诉自己就看一眼马上回去工作,结果却被一篇文章吸进去。如果发了评论,就会不停地刷有没有人回复。
通常如果文章在 Hacker News 或 reddit 上火了,我会专门留一天来回复评论。我的 ZFS 文章并没有大火,但我还是花了一整天回评论。
我曾用过一个叫 LeechBlockNG 的浏览器扩展来克制不良的社交媒体习惯,但它似乎会泄露内存、拖慢整个浏览器,所以就禁用了。现在我又重新启用了它,暂时还没发现内存泄漏,也许这次能行得通。
解决办法:再给效率类浏览器扩展一次机会,给那些浪费时间的网站增加一些阻碍。
从睡眠中断中恢复
我的幼儿睡不好,导致我和妻子也睡不好。
睡眠中断本身可能还不是最大的问题。更关键的是,我会拿睡眠不好当借口来偷懒,比如跳过写作或去刷社交媒体。心里会想:“昨晚睡得这么差,今天就不用那么拼了吧!”实际上我本可以保持 80-90% 的效率,却用睡眠不好来为自己开脱。
解决办法:不再把睡眠中断当作偷懒的借口。
拖延付费编辑工作
作为《Refactoring English》提供的服务之一,我一直在做编辑工作。
我喜欢帮别人改博客文章,但这很耗费心力。改自己的文章已经很难,给别人改就更难。改自己的东西时,我可以凭感觉改,不必解释为什么这么改;而给其他博主提修改意见时,就必须说清我在初稿中看到的不足,以及为什么我的建议更好。
我发现自己一直在拖延编辑工作,即使一天内能做完,也会拖上好几天。而在拖延编辑工作时,我自己的写作也会跟着被拖延,因为我想优先完成客户的工作。
解决办法:意识到拖延编辑工作会浪费大量时间,尽快着手处理。
业余项目
用 10 小时以静态网站生成器重写一个曾耗时 300 小时的 Vue 应用
2019 年,我尝试做过一个叫 What Got Done 的产品。它能让团队成员相互分享每周的工作总结。

What Got Done 是一个让人们分享每周工作进展的网站。
在 Google 工作时,内部有一个叫 Snippets 的工具,功能和 What Got Done 一样。我很喜欢它,离开 Google 后也一直坚持写周报,即便当时是一个人工作。
我始终没能为 What Got Done 找到客户,于是将其开源,在过去六年里作为业余项目维护着。
最初我用 Vue、Firestore 和 AppEngine 搭建了 What Got Done,而现在我已经非常不喜欢这些技术。我花了很长时间把 Firestore 换成 SQLite、把 AppEngine 换成 fly.io,但 Vue 一直留着,让开发体验很不愉快。
每周我都会在 What Got Done 上发布更新,同时越发怀念用 VS Code 和 Hugo 写博客的工作流。于是某个周末,我干脆用 Hugo 把 What Got Done 重新实现成了一个简单的静态网站,现在托管在 weeks.mtlynch.io 上。

我用 Hugo 将 What Got Done 重新实现为一个可生成的静态网站。
六年里,我大概花了约 300 小时将 What Got Done 实现并维护为一个 Go + Vue + SQLite + fly.io 的应用。而用简单的 Markdown 文件加 Hugo 重写成静态网站,只花了 10 小时。
因为新版本只是一个自用应用,我可以加入一些个性化功能,比如从 git 提交记录自动预填周报。当然,作为一个纯静态网站,托管、维护和备份都比原来那个前端、后端、数据库各一套技术栈的完整网页应用要简单、便宜几个数量级——它只需要源码管理即可。
逐步关停 What Got Done
我不想永远维护 What Got Done,尤其是在我自己都不再使用的情况下。
尽管 What Got Done 只有少数活跃用户,我还是不愿抛弃那些曾使用我产品的人,所以我尽量把 What Got Done 的下线体验做得友好一些:
- 我在网站上宣布 What Got Done 将于年底停止运行。
- 增加了一项让用户以 Markdown 格式导出文章的功能。
- 反正我也要把自己的数据迁移到 Hugo,顺手就把这个功能做进了网页应用,让所有用户都能导出。
- 增加了一项让用户设置转发地址的功能,以便在 What Got Done 关闭后继续访问。
- 例如,我已将自己的个人主页
whatgotdone.com/michael设置为永久重定向到 weeks.mtlynch.io。
- 例如,我已将自己的个人主页
用 Gleam 编写的 AIM 日志解析器的进展
我仍在通过摆弄一个解析高中和大学时期旧 AIM 日志的小工具来学习 Gleam 编程语言。最基础的日志看起来是这样的:
Session Start (AIM - DumbAIMScreenName:Jane): Mon Sep 12 18:44:17 2005
[18:44] Jane: hi
[18:55] Me: hey whats up
Session Close (Jane): Mon Sep 12 18:56:02 2005
解析时间戳
六月时,我的解析器已经能在基础层面工作,能把上面的日志提取出发件人和消息内容,如下所示:
[
Message(sender: "Jane", body: "hi"),
Message(sender: "Me", body: "hey whats up"),
]七月我取得了进展,解析器现在能理解时间戳了,这有点棘手,因为需要把会话元数据中的日期和消息中简单的 HH:MM 信息结合起来。因此,我的日志解析器现在能把上面的日志转换成这样:
[
Message(
timestamp: must_parse_rfc3339("2005-09-12T18:44:00-04:00"),
sender: "Jane",
body: "hi",
),
Message(
timestamp: must_parse_rfc3339("2005-09-12T18:55:00-04:00"),
sender: "Me",
body: "hey whats up",
),
]将词法分析和解析合并为一步
我还简化了解析器,改为单遍处理,而不是分开进行词法分析和解析。
我一开始觉得把日志先拆成 token 列表再解析会更规范、更优雅。也就是说,解析器不是直接看到一行像 Session Start (AIM - DumbAIMScreenName:Jane): Mon Sep 12 18:44:17 2005 这样的内容就去解析,而是希望词法分析器先把它变成一系列 token,比如:
[
SessionStart,
Word("(DumbAIMScreenName:Jane)"),
ColonSpace,
Word("Mon"),
Word("Sep"),
...
]但这意味着我需要一个隐藏的第一遍处理,把字符串切成词法分析器能识别的子串,比如 ["Session", " ", "Start"],而且我还得自己实现字符串切分逻辑,因为 Gleam 的内置库没有办法按子串切分字符串的同时还保留分隔符。例如,如果先按换行再按空格切分,就会得到一个字符串列表,却无法分辨分隔符到底是空格还是换行。
感觉我实际上把输入解析了三遍:一遍是自定义的字符串切分、一遍是词法分析,以及一遍真正的解析。我起初以为这是因为我对函数式语言或文本解析器了解不够,总会找到更优雅的词法分析和解析方式。
我借着这份困惑,终于说服自己买了一本纸质版的Crafting Interpreters,这是我见过设计最精美的软件类书籍。

我的 Gleam 项目终于给了我一个购买纸质版Crafting Interpreters 的理由。
读完书中关于词法分析的章节后,我得出结论:我的 AIM 日志结构化程度不够,不适合做词法分析。我尝试把所有逻辑合并到一个解析器中,让它逐字符读取输入,这样感觉更简单。
逐字符解析的缺点是,Gleam 的模式匹配看起来丑了不少。在旧的实现中,我可以像这样匹配字符串模式:
case contents {
["Session", "Start", ..rest] -> tokenize_list(rest, [SessionStart, ..acc])
["Session", "Close", ..rest] -> tokenize_list(rest, [SessionClose, ..acc])
而现在则是更长、更混乱的模式匹配:
case state.remaining_graphemes {
[
"S",
"e",
"s",
"s",
"i",
"o",
"n",
" ",
"S",
"t",
"a",
"r",
"t",
...
我是不是在把类硬塞进 Gleam?
随着解析器的推进,我发现自己在反复编写签名相同的函数:
fn parse_tokens_with_messages(
tokens: List(Token),
messages: List(Message),
) -> List(Message) {
我的很多函数都接受相同的参数、返回相同的值,而且随着代码增多,参数和返回值的列表也越来越长。
于是,我创建了一个 ParseState 类型,并改为传递它:
type ParseState {
ParseState(
last_timestamp: timestamp.Timestamp,
messages: List(Message),
remaining_graphemes: List(String),
)
}
fn parse_graphemes(state: ParseState) -> ParseState {
但这感觉就像把面向对象的类偷偷塞进了 Gleam。因为如果用 Go 来写,代码会是这样:
type Parser struct {
LastTimestamp time.Time
Messages []Message
RemainingGraphemes []rune
}
func (p Parser) Parse() {
函数式编程大佬们:我这算作弊吗?还是说这就是在函数式语言中传递状态的正确方式?
收尾
完成了什么?
- 发布了“Underused Techniques for Effective Emails”,并向抢先体验的读者发送了扩充版。
- 将《Refactoring English》最后一批仅在网页上的内容迁移到了电子书中。
- 发布了博文《将 ZFS 存储池从 RAIDZ1 迁移到 RAIDZ2》。
- 为 ScreenJournal 创建了更完善的密码重置流程。
- 为 PicoShare 上的访客增加了文件过期选项。
- 为一篇即将发布的博文提供了无偿编辑,以换取将反馈公开作为编辑服务的宣传。
- 制定了 What Got Done 的关停计划,并将数据迁移到了 weeks.mtlynch.io。
经验教训
- 并非所有《Refactoring English》的早期读者都愿意提供反馈也没关系。我可以继续联系读者,找到那些愿意更积极参与的人。
- 评估自己浪费时间的方式并思考应对措施,总是有益的。
下月目标
- 给 20 位未曾交流过的读者写个性化邮件。
- 发布《Refactoring English》的新章节。
- 完成剩余的营销任务。
求助
如果你是一位想提升写作能力的开发者,或认识有此需求的人,欢迎与我联系。
随机一篇博客
评论
登录后参与讨论