《重构英语》第 8 个月
一句话总结
也许并不是每个人都想加入我的焦点小组。
要点
- 我发现,并非每一位购买我这本书抢先体验版的读者都愿意对粗糙的草稿给出反馈。
- 我弄清了自己的时间都花在了哪里,并思考如何减少时间消耗。
- 我花了 10 个小时从零重写了一个最初花了 300 小时才建成的 Web 应用。
- 我继续用 Gleam 学习函数式编程,但我可能是在作弊。
目标评分
每个月初,我都会宣布自己想要完成的目标。以下是我对这些目标的完成情况:
与至少 10 位从未交流过的读者对话
- 结果:给七位读者发了邮件,收到三封回复,进行了一次实时对话
- 评分:C
在坐下来统计之前,我以为回复率要糟糕得多,但实际上很多读者都在回复。问题在于我主动联系得还不够多。
我可能在每封邮件上花的时间太长了,总想说出一些独特的、明显不是 AI 生成的内容,结果一头扎进对方的博客里读了一个小时。
清空营销点子的积压清单
- 结果:完成了大约 70% 计划要做的事
- 评分:C
大部分任务我都完成了,不过我还没来得及发布一年前录制的一篇访谈,希望能尽快搞定。
发布《重构英语》的新章节
- 结果:发布了《被低估的高效邮件写作技巧》
- 评分:A
我对这一章的成果很满意,但我知道把它分享到社交媒体上是一场赌博。它在 Lobsters 上反响不错,但在 Hacker News 上却毫无水花。
《重构英语》数据指标
| 指标 | 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%) |
网站访问量上升了,这很好,因为我并没有发布什么热门新文章。我认为这是一个积极的信号:仅靠人们阅读现有的样章,访问量就能保持健康。
总收入略有下降,但这只是因为我的付费咨询工作从一单变成了零单,所以变化不大。更让我兴奋的是,预购收入比六月增长了 34%。
如果大家喜欢的只是买书的那种“感觉”呢?
在我联系读者并与他们视频通话的过程中,有一条反馈我反复听到:“我还没开始读呢。”
说这话的人里,有几天前刚购买的,也有拿到书好几个月的。
我最担心的是,这本书是“维生素而不是止痛药”。人们把好的写作视为一件重要但不紧急的事。他们可能会在即将到来的面试前读一本叫《如何搞定你的下一场编程面试》的书,但提升写作这件事很容易被无限期推迟。
我在写作之前就预售这本书的部分动机,是想看看是否有足够多的人愿意为它付钱。确实有,而且人们还在继续购买,但我担心这可能不像我想象的那样是一个有预测性的信号。
万一我的书就是那种人们买了之后感觉自己在为写作投资、实际上却没有投入任何时间去做的事呢?万一它就像 Planet Fitness 那样——那家据说是靠办了卡却从来不去健身的人赚钱的热门连锁健身房?

万一我写的是图书界的 Planet Fitness 呢?
照片由 Mike Mozart(迈克·莫扎特)拍摄,依据 CC-BY-2.0 许可证使用
即便我能成为图书界的 Planet Fitness,那也不可持续。要让这本书在经济上站得住脚,我押注的是读过并且喜欢这本书的人的口碑推荐。我希望热门博主会把我的书当作对他们有帮助的资源来引用。当一位开发者说要提升写作水平时,我希望我的书是大家显而易见会推荐的那一本。
也许顾客并不想加入我的焦点小组
我预售这本书的另一个原因是,我预测预购的读者会特别热心地在写作过程中提供反馈。
通过与同样是顾客的朋友们交谈,大多数人说他们预购是为了支持这个项目,想等书写完后再读,但他们并不一定想参加焦点小组或对草稿给出反馈。他们只想读最终完成的版本,这完全合理。
我应该继续一个一个地主动联系读者
通过逐个联系顾客并安排实时通话,我发现有些读者非常热情,愿意提供反馈。我只找到了少数几位,但他们给了我极好的反馈。
我发现找到热心读者最有效的方式就是逐个给他们发邮件,所以我会继续这样做。
我的时间都去哪儿了?
每隔 一段 时间,我回顾上个月完成的事情时就会想:“等等,为什么我上个月只做了这些?剩下的时间我都干什么去了?”
七月就是这样一个月。我照常工作了全职的时间,但回顾这个月时我在想:“我怎么只写完了一个新章节?”
那么,让我回头想想,在不做月度目标相关工作的那些时间里,我的时间都花在了哪里。
在章节上过度投入
几个月前,我意识到自己在文字打磨上花了太多时间,而不是在章节达到 80%-90% 完美时就先交给读者。我的解决方案是给每一章设定时限,在时间预算内能写多少就写多少。
对于七月写的邮件那一章,我的预算是五个小时,但实际花了 17.5 个小时。其中一部分是有意为之,因为设定目标后我意识到这一章很适合作为独立样章放在网站上。先写成一篇样章再把它整合回书中,需要更多时间。
但在我目前正在写的章节上,我也犯了同样的毛病。6 小时的预算已经用了 6.5 小时,而且在我觉得可以放心交给读者之前,大概还剩至少 3 个小时的写作量。我觉得这既是我打磨过头的问题,也是我为全书第一章——最重要的一章——设定的预算太低的问题。
解决方案:为会有公开样章的章节预留额外时间,并把写作时间严格限制在规定的限度内。
计划外的博客文章
我喜欢在学到东西后不久就用博客文章记录下来,但就算每周花 20 个小时写博客,我也永远写不完所有想写的文章。
我的一些博客文章能吸引可能会读我这本书的人,而另一些则未必。对于“纯粹为了好玩”的文章该投入多少时间,我必须有意识地权衡。
上个月,我发布了一篇新的博客文章,《将 ZFS 存储池从 RAIDZ1 迁移到 RAIDZ2》。我知道它对书的推广没帮助,但我也以为只需要几个小时就能写完,而且我要解释的东西还没有别人解释清楚过。
实际上,这篇 ZFS 文章花了七个小时才写完,作为一篇“纯粹为了好玩”的文章,这个投入不太明智,尤其是在我这个月没有一篇更能讨大众欢心的《重构英语》章节可分享的情况下。
解决方案:对非书籍相关的博客文章更有选择性,优先写那些能吸引也会读我这本书的读者的文章。
糟糕的社交媒体习惯
每当我感到无聊,或者缺乏动力去认真思考一篇难写的文章或一段难写的代码时,我就会去刷社交媒体。我告诉自己只是快速看一眼就回去工作,结果却被一篇文章吸了进去。如果我写了条评论,接下来就会不停地强迫性地查看有没有回复。
通常,如果我的文章在 Hacker News 或 reddit 上引起大反响,我会专门留出一天来回复评论。我的 ZFS 文章并没有引起大反响,但我还是花了一整天回复评论。
有一段时间,我用过一个叫 LeechBlockNG 的浏览器扩展来遏制糟糕的社交媒体习惯,但它似乎会内存泄漏并拖慢整个浏览器,所以我禁用了它。现在我又试了一次,还没发现内存泄漏,也许这次它能派上用场。
解决方案:再给效率类浏览器扩展一次机会,为那些浪费时间的网站增加访问阻力。
从睡眠中断中恢复
我家蹒跚学步的孩子睡不好,这意味着我和妻子也睡不好。
睡眠中断本身可能只是问题中较小的一部分。更重要的是,我会拿睡眠中断当借口来放纵自己,比如跳过写作时段或去刷社交媒体。我会想:“我今天不该这么拼命。昨晚睡得那么差!”而实际上,我可以以平时 80%-90% 的效率工作,但我用睡眠不好来为自己的偷懒开脱。
解决方案:不再把睡眠中断当作偷懒的借口。
拖延付费编辑工作
我一直把编辑服务作为《重构英语》提供的配套服务之一来做。
我喜欢编辑别人的博客文章,但我觉得这在精神上很耗费心力。编辑自己的文字已经很难了,替别人编辑就更难。改自己的稿子时,我可以凭感觉修改,不必解释为什么要这么改。而当我给其他博主提编辑意见时,我必须清楚地表达出我在他们的草稿中看到了哪些不足,以及为什么我认为我的建议更好。
我发现自己一直在拖延编辑工作,即使一天之内完全有时间完成,我也会拖上好几天。而且当我拖延编辑工作时,其实也在拖延自己的写作,因为我想把客户的工作排在自己的前面。
解决方案:认识到拖延编辑工作会消耗大量时间,并尽早着手处理。
副业项目
用 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 的,而现在我对这三项技术都非常反感。我花了很长时间用 SQLite 替换 Firestore、用 fly.io 替换 AppEngine,但 Vue 一直留着,让开发过程很不愉快。
每个星期,我都会往 What Got Done 上发更新,同时想着自己更喜欢用 VS Code 和 Hugo 写博客的工作流。于是某个周末,我直接用 Hugo 把 What Got Done 重写成了一个简单的静态网站,现在托管在 weeks.mtlynch.io。

我把 What Got Done 重写成了一个可以用 Hugo 生成的静态网站。
也就是说,六年来我大概花了约 300 个小时来实现和维护这个 Go + Vue + SQLite + fly.io 应用。而用简单的 Markdown 文件和 Hugo 把它重写为静态网站,只花了 10 个小时。
因为新版本只是一个自用的应用,我可以添加个性化功能,比如根据我的 git 提交记录自动填充每周更新。当然,它的托管、维护和备份也要简单和便宜几个数量级,因为它只是一个带版本控制的静态网站,而不是前端、后端和数据库各用一套技术栈的完整 Web 应用。
逐步关停 What Got Done
我不想永远维护 What Got Done,尤其是现在连我自己都不用它了。
尽管 What Got Done 只有寥寥几位活跃用户,但我讨厌抛弃那些开始使用我所提供服务的人,所以我尽力让 What Got Done 的退出体验做得体面:
- 我在网站上宣布 What Got Done 将于年底停止运行。
- 我添加了一个功能,让用户以 Markdown 格式导出自己的帖子。
- 反正我迁移数据到 Hugo 时也需要这个功能,所以我想不如把它做成 Web 应用本身的功能,让任何用户都能使用。
- 我添加了一个功能,让用户可以设置一个转发地址,以便在 What Got Done 关闭后继续使用。
- 例如,我已经把我的个人主页
whatgotdone.com/michael配置为永久重定向到 weeks.mtlynch.io。
- 例如,我已经把我的个人主页
Gleam 版 AIM 日志解析器的进展
我仍在通过折腾一个解析器来学习 Gleam 编程语言,解析的是我高中和大学时代的旧 AIM 聊天记录。最基本的日志长这样:
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 列表再解析这些 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 项目终于给了我一个购买纸质版《手写解释器》的理由。
读完这本书的词法分析章节后,我得出结论:我的 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() {
函数式编程的行家们:我这是在作弊吗?还是说这就是在函数式语言中传递状态的正确方式?
收尾
完成了什么?
- 发布了《被低估的高效邮件写作技巧》,并向抢先体验读者发送了扩充版。
- 把《重构英语》最后一批仅存在于网页上的内容迁移进了电子书。
- 发布了博客文章《将 ZFS 存储池从 RAIDZ1 迁移到 RAIDZ2》。
- 为 ScreenJournal 创建了更好的密码重置流程。
- 为 PicoShare 添加了访客文件过期选项。
- 为一篇即将发布的博客文章做了免费编辑,作为交换,对方同意把我的反馈意见发布出来,为我编辑服务的宣传。
- 制定了 What Got Done 的关停计划,并把我的数据迁移到了 weeks.mtlynch.io。
经验教训
- 并不是每一位《重构英语》的早期读者都想提供反馈,这没关系。我可以继续联系读者,找出少数愿意更深入参与的人。
- 审视自己浪费时间的方式并想办法减少浪费,总是有用的。
下个月的目标
- 给 20 位从未交流过的读者写个性化的邮件。
- 发布《重构英语》的一个新章节。
- 完成剩余的营销任务。
求助请求
如果你是一位有兴趣提升写作水平的开发者,或者你认识这样的人,欢迎与我联系。
随机一篇博客