2025-12-05 15:43:00
这本书有两个版本的封面,下图左侧的是西西弗特供版封面,实际上是包裹在原封面的单独的一层。不得不说,如果西西弗没有出这个独家的封面,我是不会买日漫感这么浓的一部小说的(没有不好的意思,只是个人不喜欢)。

这本书有同名电影,是 2021 年上映的动漫短片,由 loundraw 执导。动漫并非是小说改编的,事实上这本书的作者栏写了两个人,宣传时也声称是乙一和 loundraw 合作推出的。我不清楚两者到底是什么关系,国内有关这本书的讨论也不多。
如果只当作一部乙一的小说来看,这本《夏日幽灵》平庸得令人讨厌,阅读时能感受到乙一并没有写得很用心,大概是因为这是别人的故事,而不能完全说得上是乙一的原创作品。夏天、烟花和尸体的要素很难让人不怀疑是不是在碰瓷乙一的名作《夏天、花火和我的尸体》,只不过这部作品不是「藏尸体」而是「找尸体」。
小说的情节是三个想死的高中生在自杀论坛上认识了,然后开始讨论名为「夏日幽灵」的都市传说,据说在夏天到某个废弃的机场燃放烟花,就能够遇到幽灵。这三个人分别是被母亲控制的优等生友也、受到校园霸凌的葵和得了绝症即将去世的凉,他们听说夏日幽灵是自杀而死的女性,所以他们想要问问她:「死是什么感觉?」传说应验并见到真名为佐藤绚音的幽灵之后,他们得知对方实际上是被谋杀的,但尸体一直没有被找到,所以被视为失踪人口,而母亲一直在等她回家。三个高中生由于灵魂渴望死亡,所以能在见到幽灵时,暂时地将肉体与灵魂分离,以幽灵形态飞行;他们决定帮佐藤绚音找到她的遗体,将她带回给母亲,完成他在这世上未了的心事——最终,很容易预料的是,这三个高中生也治愈了自己,除了被病魔击败的凉,葵和友也都选择了活下去。
确实有乙一的风格,但完全可以做得更好。这本小说最主要的问题是人物太扁平、太抽象,情节也有很多部分是一笔带过。一些心理描写的确能引起我的共鸣,比如第 83 页,友也坐在母亲的车上接受教育的时候,感到一种像是尸体被塞进行李箱一样的窒息感,而且有一种快要爆发出来的逃离这个封闭空间的冲动。我之所能感同身受是因为我经历过,但小说中有不少我没经历过的情感体验,显得过于生硬,因为看起来乙一根本没有认真描写。
她(佐藤绚音)将母亲的手跟自己的重叠在一起,这个举动让人感受到了爱意。(P66)
「这个举动让人感受到了爱意」是一个职业作家能写出来的句子吗?这种爱意不应该是作家用文字勾勒出来的吗?如果小说这么写,那就和恐怖片导演只在屏幕上写几个大字「你应该感到害怕」没有任何区别。
除了生硬的句子,小说里还有很多陈词滥调,这些词句不仅毫无新意而且过于直白,把人物形象和文化现象当成迷因一样传播,堪比短视频。
“我没有看出来你是真心感到歉意,只是迎合他人吧?”我被吓了一跳,这的确是我一直以来的感觉。(P23)
“数学有一道题,你是不是因为粗心大意才没得满分?你好好反省一下吧。”(P31)
顺带一提,友也的母亲也是一个非常扁平、模版化、标签化的东亚家长,各种「为你好」的陈词滥调也是最容易博得共情的。这样标签化的东西,叫做「内容」而不是「故事」。我完全没有读到这个母亲背后复杂的心理、性格、行为动机和思维方式,她只是一个随处可见的爱孩子但要求严苛的东亚母亲,是一个模板化的文化产物——真要看这种东西,为什么不去刷短视频?
佐藤绚音,也就是夏日幽灵,她的人物形象非常抽象,难以把握,一会儿她是个神秘感十足、让人觉得看透了世界的女幽灵,一会儿她是个喜欢开玩笑、挑逗男孩子的大姐姐;她时常表现得对什么都不在乎,又会对人和事表达一种「大姐姐」形象应有的关心,还会在想到母亲时感到伤感自责——这些东西要是没有完整的背景故事,我很难买账,只会觉得 OOC1,整部小说有关夏日幽灵的故事就只有「她和妈妈吵架,一气之下出门被车撞了,然后被塞进行李箱埋了」。她究竟是为什么要和妈妈吵架啊!她和妈妈的关系究竟是怎样的?难道就只有通过抽象的「女儿应该爱妈妈」的道德规训才能让我理解这个人物的行为逻辑吗?!
既然原本是动漫,而主角又是高中生,自然就少不了青春浪漫恋爱情节。即使是恋爱要素,也像是硬塞进去的,完全没有展开,整部小说里可能就只有五六句话提了一下——友也好像是喜欢上了幽灵吧,但他这个喜欢美术的理科直男自己也搞不清楚;葵和凉开始交往了,尽管两个人只是一起交流自杀方法,为了找幽灵和尸体聚了聚而已,但不管怎么样作者说了他们谈恋爱了就是谈了,不管你会不会觉得突兀,也不管这个恋爱对小说情节以及人物塑造有没有丝毫的帮助。
幽灵劝友也不要自杀的方式有点好笑,但确实很日漫,也大概会有很多人喜欢。
“因为我喜欢比我年长的人。”
她的回答出乎我的意料。2
“友也,你好好想一下。如果你现在死掉的话,就会永远比我小。老实说,高中生并不在我的选择范围内。再多活些时间,等你变得比我大了再去死吧。我是不会说你坏话的。等你变成一个讨人喜欢的大叔后,再来找我吧。”
怪不得全文没有提及幽灵的父亲呢,原来也有 daddy issue 啊。
不过这段对白发生在友也的梦里,解读为高中生做春梦会更合理一些,不然前面的这一段就显得怪怪的:
“只要见到冷静的男孩子开始动摇,我就会感到激动。”(P52)
老实说我也很喜欢呢。 😋
这类有些调皮的大姐姐的形象确实挺适合出现在动漫里的,不少人都会喜欢的。我作为同性恋一开始不太能理解,后来我想象了一下,如果有一个阳光开朗还有些深沉的幽灵大哥哥带着我到处飞的话,那确实还挺爽的。
这本小说似乎原本是动漫的脚本,我的想法是,既然是写给动漫的,那就只让它成为动漫就好了。我相信这个故事用动漫展现出来会是很好的作品(尽管我没有看过这部短片),小说里有很多一笔带过的部分,如果能有更多的视觉信息,就不会像我前面所说的那样生硬和抽象了。至于直白的部分,动漫不应该就是这样的吗?修辞是文学创作才会用的手法。
另外这本书的印刷也值得吐槽,字体、行间距、字间距、页边距都很大,一共 159 页,很快就翻完了,就算是产出内容而不是讲故事,内容量也显得毫无诚意。
总而言之,还不错的故事,很平庸的小说。如果你喜欢乙一,不要读这一本。如果你想了解这个故事,去看动漫吧,我敢肯定动漫画得比小说写得更好。
结束之前,还是来说些好话。抑郁、被孤立、有轻生想法的未成年人形象,几乎是乙一创作的舒适区(虽然「舒适」这个词用在这里怪怪的)。尽管小说的整体质量不能让我满意,但一些细节能让我感受到「他是真的懂自杀者的想法」。
比如,第 122 页里,凉随口问了一句「人生究竟有什么意义?」,而友也最终选择了保持沉默,这是他的心理活动:
要是我轻率且像煞有介事地回答,他一定会轻视我的。
这种「轻率」和「煞有介事」,正是我在《 永远不要提醒自杀者关心家人 》中批评的那类站着说话不腰疼的规劝甚至规训轻生者的说话方式。就算不谈生命与死亡,这种态度也令人讨厌,而很多人都喜欢这样说话。读到这里我有种找到嘴替的感觉。如果要证明一个数学结论,数学家必须要给出详尽的证明过程和自己使用的一套公理(Axioms),人们看到这些必要的信息自然会感到信服,或者有根据地提出质疑;然而,「轻率且像煞有介事」的这种态度,就像是把自己当成上帝代言人一样,觉得自己说出口的话无需证明,本身就是如此,而对方应该明白,不明白就是对方蠢,而不是自己没有把话说清楚。真的令人讨厌。
友也的人物形象有一定的典型性,只可惜小说没有深入剖析了。友也对母亲的歉意、绝对服从和不能决定自己人生道路的无能感,被乙一理所当然地写了出来,那些没有经历过类似挣扎的人不容易理解。这里引用一条豆瓣上的对电影的评价:
主角是那种父母花钱给你上补习班帮助你高考(日本社会不是普通家庭都有钱上补习班的好吗),但自己想考艺术,又不跟父母沟通,整天觉得世界都灰暗了就想自杀的那种。所有剧情都充斥着青春期年轻人的拧巴拧巴拧巴拧巴。
「不跟父母沟通」当然是主角自己的问题,但「被亲情和愧疚束缚而变成提线木偶」的确是很典型的年轻人的拧巴,后面主角也支棱起来回去复读准备考艺术学校了,故事想表达的或许就是摆脱这种拧巴的过程。立意还不错,但还是得吐槽:描写不够细腻,不仅没有诚意,还让好多人无法与主角共情。
关于友也这一形象,另一个有趣的点是他对绘画的态度。他的母亲极力反对他画画,于是他只能偷偷地素描,而本子被母亲发现之后,他被要求一页一页亲手把画作撕掉扔进垃圾桶并保证不会再画,必须全力准备考试。为了不惹母亲生气,保全自身,他照做了。撕毁画作时他感到麻木,后来他把这种行为定义为「背叛」:
或许正是我对绘画的这种背叛行为导致自我厌恶,才使心中的创作欲望溜走了吧?
这句话我有些同感,所以觉得耐人寻味。如果读完这本小说要我只留下一句话,我会选这一句。
好了,好话说完了,总之还是不推荐读。
2025-12-04 00:05:00
「少即是多」的哲学随处可见,但这种思想已经变成不入流的风格了,喜欢的人不假思索地践行着(每次谈到极简,我都忍不住要批评那些只用了白底黑字和卡片设计就自称极简的网站),不喜欢的人自然也看不上。极简这个词延伸到了各个领域,从装修到笔记方法,以至于成为了一种生活的风格,即生活方式(lifestyle)。
与其在丢掉某个东西的几个月后突然发现自己需要它,不如真的思考一下自己为什么需要把东西变少,毕竟极简本身并不是目的,也不是褒义词。「少即是多」脱离了语境是不成立的,今天的文章就来分析适用这条原则的语境之间的共性是什么。
熵(Entropy)这个词被滥用了,不过这个原本来自于热力学的专有名词实际上在信息论中确有定义1,它是对不确定性的度量。本文不讨论严谨的数学,仅仅借用信息论中的概念,在极简主义的语境下使用「熵」这个词。简单来说,信息越多,我们就越不确定我们要干什么,做出决策也就越困难,因为可供选择的可能性太多了。
如果走进一间房间,房间里只有一个沙发和放在沙发上的一本书,房间给人提供的信息量是很少的,可能性也就很少。在这样一间房间里,一个人能做的就是坐在沙发上读书(排除掉一边用手指转书一边在沙发上跳舞这种疯癫选项之后)。如果这间房间里除了沙发还有盆栽、浇水壶、咖啡机、黑胶机、空气净化器和零食柜,随着信息量的增加,人的行为的可能性也就增加了,而且增加的速度并不是线性的。信息熵越高,选择就越多,做出决策所需要消耗的心理资源也就越大。看房之所以令人痛苦,就是因为可以选择的房源很多,而且每间房里需要考虑的要素太多了;如果超市里只售卖一种抽纸,那买纸巾就会变的非常简单——有时候,没得选也是一种幸福。
经验之谈,熵在不刻意控制的境况下会增加。如果不刻意控制自己的消费,房间里就会多出来很多东西;如果看到有意思的 App 就下载,手机屏幕上就会多出来很多图标;如果社交不克制,通信软件里的联系人就会一直增加;如果一直追求增长,产品里就会多出来很多臃肿的功能;如果一直在网上冲浪,浏览器里的标签页就会一直增加,电脑屏幕上就会打开越来越多的窗口…… 熵在自然、不受控制、不被观察的情况下,会一直增加,人做决策也就会越来越困难,耗费的心力会更多,理解信息的成本会变高。在某种程度上,容忍自己视线里的信息熵增加,就是对目之所及之物缺乏控制力。
在我看来,所谓的极简主义(minimalism)追求的就是将信息控制在最小(minimal)的可用范围内。一个很好的例子是我在 第 50 期周刊 中分享的一个得体的网站所需要的最少 CSS 样式2,这是作者 Kevin Powell 给出的答案:
html {
color-scheme: light dark;
}
body {
font-family: system-ui;
font-size: 1.25rem;
line-height: 1.5;
}
img,
svg,
video {
max-width: 100%;
display: block;
}
main {
max-width: min(70ch, 100% - 4rem);
margin-inline: auto;
}
如果阅读 Kevin 的这篇文章,你会发现他并不是在做减法,而是从零开始做加法,而做加法的标准是「只有真正必要的部分才被加进来」。文中,Kevin 列举了网站常用的元素,并根据经验论述了这些元素分别需要什么样的样式,以及为什么需要这些样式(比如优化排版、防止图片溢出、实现简单的夜间模式、限制页面宽度等)。这就是一种控制:清楚地知道自己的目的——拆解实现这个目的必要步骤——只执行必要的步骤。
对我个人而言,我希望我的设备完全在我的掌控当中,所以我会不定期查看 MacBook 上的 App 列表,卸载不常用的 App;由于命令行工具不像 GUI 应用那样方便查看,我还开发了
wthis
这个小工具用于直观地展示某个 Homebrew 软件包的描述、是何时安装的、是自动作为依赖安装的还是手动安装的、与其他软件包的依赖关系等必要信息,以确保我留在 brew list 里的都是我清楚用途并认为应该保留的软件。这种控制的欲望,表现出来的就是所谓的「极简主义」。只不过,我在实践时所想的并非「尽可能删掉更多东西」,而是「只能够保留我认为必要的东西」。两者的区别很微妙,但绝非相同。
既然极简主义是目的明确且只保留必要步骤的自然产物,那其实就不应该有「什么时候才应该极简?」这个问题。对于一些人来说,把家里用各种装饰填满能让心情愉悦,但另一些人却看到不这种乱中有序,一些崇尚极简主义生活的人的家里空无一物,物品全部都收纳到柜子里了,而这种整洁能让他们心情愉悦。如果「让家变得令人心情愉悦」是目的,而一些人实现这一目的的必要步骤就是在家里的各个角落摆上自己喜欢的东西,那要我说,这种繁杂的生活空间所表现出来的,依旧是极简主义。
真正应该摈弃的,是文章一开始提到的「任由信息熵在自己的视线内增长而不加以控制」,例如,就算家里摆放的东西不多,拆掉的快递盒到处丢,碗筷扔在水槽里不洗,这依旧是一种繁杂——不过,要是有人觉得快递盒就应该堆在各个角落,等堆积到一定程度再统一处理,这种外人眼里的杂乱如果是个体思考后得出的解决方案,在我看来,就不是一种任由熵增发生的行为了。总而言之,极简的本质是控制,而控制就是观察、思考并做出行动加以干预的过程,如果个体思考后得出结论认为不需要进行干预,那就不算是有问题。
任由熵增发生的例子有很多,比如家中凌乱自己却浑然不知,就是缺乏「观察」这个步骤,根本没有尝试掌控自己的生活;或者做出了「观察」,但只接收到了信息而没有「思考」,也是缺乏控制的体现;如果已经做出了「我或许应该收拾一下」的思考,而「干预」始终没有发生,也是同样的道理。
所以,真正的问题是:这个东西我看到了,它应该在这里吗?我是不是应该保留它?或者,在一开始就要问:我有了增加某个东西的想法,这是必要的吗?我应不应该增加它?
回忆我早年间使用 Typecho 搭建博客的经历,那个时候刚学会前端和 PHP,经常在网站的各个角落添加各种没用的元素,比如在页脚添加一个会跳动的颜表情、建站时间正计时、各种徽章图标(说真的,页脚大概是最容易被塞爆的区域了)。倒不是说这些元素本身不好,重点在于,当时我的一心为了折腾,想要变得和别的博客一样,我从未想过某些改动是否真的必要——思维过程的缺失才是致命的。在我看来,过度依赖 LLM 进行 Vibe Coding(或者 Vibe Designing 等一系列把创造活动交给 LLM 等行为)最严重的后果是自己主动让渡了控制权,根本没有想过某一段代码、某个模块是否必要,而 LLM 最擅长的就是生成长内容。
如今的极客死亡计划看起来似乎符合不少人对极简主义设计的定义,但我不会说它是一个极简的网站,我会说:它是一个完全受我控制的网站。
2025-12-01 23:33:00
我在《 搬家如何帮我理解现代人与消费的关系 》中,是这样定义消费行为的:
- 消费实用信息。 - 我记得我在小红书上反复搜索「货拉拉价格」「货拉拉能进学校吗?」和「怎么在菜鸟驿站借小推车」等问题。 - 我找了好几个有租房经验的朋友,试图让他们打消我的各种顾虑。 - 我在 Bilibili 上搜索「大学生校外租房」,找了各种视频看,哪怕那些内容其实与我的情况有较大差异,帮助并不大。
- 消费实用工具。
我在搬家的第一天在美团花费八十多块钱下单了小推车和纸箱,选购的时候我非常焦躁,买完我就安心了;之后,我甚至一点也不着急去取它,消费行为本身似乎就提供了某种心理安慰;实际上,搬家的时候我根本没有用到我买的这些东西。
消费是短见的应付当下情绪的行为,这些情绪往往会带来某种不适,而人们用触手可及的消费行为缓解这种不适。比如,人们通过发明短视频消解了「无聊」这一不适,通过进出商场或使用网购软件消除了「空虚」这一不适,甚至,在我看来,美国人滥用止痛药的行为也是一种消费,用于消除疼痛带来的不适。然而,无聊、空虚和疼痛,任何形式的不适,都是一种「负反馈」,用于修正行为,提醒大脑:下次要更好地安排时间和任务,这样就不会感到无事可做;我必须直面自己的情感需求,找到空虚的源头,才能避免这种深不见底的痛苦;我以后可不能这么冒失了,必须保养好自己的身体,我太怕疼了。拒绝将自己暴露在有益的负反馈下就是在任由自己变蠢。
消费不仅短见,而且产生浪费,让被消费物的价值得不到发挥。我相信现在的人们常常有这样的经历:刷了几个小时短视频(或者其他类型的信息流),终于反应过来时,却完全记不起来自己刚才看了什么。使用 ChatGPT 等大语言模型也会导致这样的「失忆」,我在 第 47 期周刊 中分享过一篇 MIT 的研究1,他们对照了大模型使用者、搜索引擎辅助者和纯人脑学习者的认知能力,发现使用大模型学习的学生根本记不住自己都写了、学了些什么。前文提出,消费行为的驱动力是情绪,为了缓解当下的短期的情绪,人们才消费,而于情绪挂钩的自然不会是理性、逻辑的和结构化的学习过程。我的想法是,短视频和大模型善于创造「学到了」的错觉,继而缓解「糟糕,我好像什么也没学」的焦虑情绪,而不是真的让自己学到了。
这种对信息和知识的消费并不只存在于短视频和大模型的使用中,还通过人养成的习惯丝滑地溜进了各个角落,比如阅读一篇网络文章只看标题、段首和高亮部分,快速阅读。如果就这样把堆积成山的阅读列表快速扫完了(甚至用 AI 总结读完),本质上就是一种消费行为——我的 RSS 阅读器有几十条更新还没看,如果我这周看不完,下周会有更多,所以我必须快点读了;我再不把这些文章读完,我的知识储备就要被别人甩好几条街了!快速扫完阅读列表能消解这些负面情绪。
用扫视和 AI 总结的方式读完几十篇文章,其实跟刷短视频没什么区别,原因如下:
消费信息除了缓解当下的不适,还让信息快速划过大脑抚平大脑褶皱,让人习惯于只输入而不思考。我写了这么多,应该已经把观点阐释清楚了:消费信息百害而仅有一利,那就是缓解不适,而这一利又会带来长期的危害——怠惰、作茧自缚和对消费行为的进一步依赖。可以说,消费简直是现代社会的合法毒品。
最开始写 稻草人周刊 的时候,我只是把它当作周期性输出的工具。当时的我刚建立起来一套笔记系统(现在一起全部推翻了,哈哈哈),缺少一个周期性复盘和输出的渠道,于是就决定每周从笔记库里筛选一个有趣的精华内容,整理、重述后放到周刊里。逐渐地,我发现写周刊是一个很好地汇总我一周所摄入信息的方式,原先,我在一段时间里度过的文章、书,看过的剧、视频,以及听过的播客,全都丝滑地经过我的大脑,哪怕认真读过,也很难留下一些太多东西;现在,这些信息就像一根根细线一样被我聚在了一起,绑成一根整齐且更粗壮的绳子,而不是像毛线球一样散成一团,最后被丢进垃圾桶或者塞到沙发底下。
在意识到自己实际上是在用写周刊的方式手动聚合信息之后,我就把《稻草人周刊》的描述改成了「一个读者的自述」。写文章时,我是个作者;写周刊时,我是个认真的读者。
写作真的能帮助我思考。我每周都固定在周一中午之前发布当周的稻草人周刊,从周二开始我就会起草下一期的周刊,并在读过、听过、看过一些东西之后往草稿里填充内容。硬性的专业知识需要长期积累,最好通过记笔记和实战的方式螺旋上升进步,而在这之外的,一个人每天会接收到的信息相当于几百张报纸、有多少 GB、是古人的几倍,早就成老生常谈了,其体量无需多言。要处理这么多的信息,而不只是让大部分从脑子上光滑地飘过,当然是需要费些力气的。
大名鼎鼎的费曼学习法,也是老生常谈了,简单来说,就是把学到的东西教给别人,以教为学。如果能把一个概念用结构化的语言描述清楚,那自己多半就是真正地理解了。把自己接收到的信息用自己的方式阐述一遍,而不只是复制粘贴或机械地敲打键盘,自然就能回避对信息的无意义消费。
这当然会更费力也更费时间,但这正好提供了一个缓冲,来保证自己不会接收过量的信息,因为接收到的信息都需要在稍后整理成结构化的语言,接收信息的速度就变低了,但同时效率也提高了,真正理解的部分变多了。我将这个原理称为「恰当阻力的艺术」,几个月前我写过一篇英文博客《 The Art of Adequate Friction 》阐述过。不要想着让所有的事情都自动化,把便捷、无阻力做到极致反而会降低大脑活动,让人不假思索地作出决定和完成任务;提供恰当的阻力,虽然会让自己慢下来,但实际的结果是,真正有用的产出变多了。
这也是我读完一本书之后会在博客上写「书评」,而不只是发「书摘」的原因。我不会绞尽脑汁地想办法把书本里的高亮标记导出(我甚至不做勾画,详见《 为什么你不应该在阅读时写批注 》),我会抄下那些我感触很深的句子(一本 250 页左右的书,这样的句子可能也就在十句左右),在写书评时将它们作为例子或阐述的对象。我在写周刊时也是如此,当我分享并用我自己的话总结了一篇文章的内容时,我会分享我自己的相关经历、体验和思考——这才是我觉得最重要、最应该记录的部分,即「我读的东西引发了我怎样的联想和思考?」
在我的周刊方法论里,即使是相比常规文章更加散乱的刊物,其中大部分的内容仍然应该是自己的思考,而不是互联网内容的剪贴簿。所以我说,这是读者的「自述」。
如果你认同我的观点,认为写周刊(或者月刊、半月刊,或者不定刊)能够帮助你停止不健康的信息消费行为,我建议你立马开始。
开始的方法很简单,你可以先用两三天的时间有意识地察觉自己接收信息的行为,例如看视频、听播客、读文章,甚至是和朋友聊天、在咖啡馆观察陌生人的行为、在洗澡时产生了浴中奇思…… 总之,当有新信息产生时,你应该把它记下来,或者至少在脑中留下一个印象。这么做之后,你应该就清楚自己每天都会从什么样的信息来源接收到什么样的信息了。
接下来,可以试着整理这些信息,不一定要像真正的刊物一样设置各种栏目,只要能用结构化的方式表示出来就好了。为了方便理解和复用,我把组成一期周刊的单元称作一个「小节」,一个小节就代表你这周接收到的一段可结构化的「信息」。一般来说,一个小节可以由四部分组成:标题、信息来源、总结和自己的思考。
假设你在这周的周三读了这一篇文章,《 Rust 背锅了:Cloudflare 故障分析 》,那么你就可以在自己的周刊草稿里加上这样一段内容:
Cloudflare 故障引发的思考
原文链接: Rust 背锅了:Cloudflare 故障分析
本文简述了本周 Cloudflare 大规模故障背后的技术原因,由于这个技术问题与 Rust 编程语言有关,导致了很多有关 Rust 的负面言论,而作者认为这些批评是不合理的,不能因为有人开车撞树受伤,就批评车的安全措施不到位。
在我看来,这次事故更多的是一次警醒和教训,提醒 Cloudflare 和所有从事相关行业的人们一定要保证代码的健壮性,不然看似无关的小小改动就可能酿成大错。
这只是一个简短的例子,你完全可以根据自己的习惯做调整和拓展。重点在最后两部分:读完之后根据留存在脑中的印象,用自己的话总结;阐述完作者的故事或观点之后,简述这段信息让你联想到了什么、体会到了什么、学习到了什么、思考了什么东西。如果文中真的有写得特别好的句子,也可以摘录下来,但最好附上自己为什么觉得它好,引用的这句话究竟是如何让自己感到印象深刻的。
这么做的目的是迫使自己主动思考,强化接收和处理信息时的大脑活动,让自己对接收的信息印象更加深刻。你可能会发现我时常在文章中提起「我在某期周刊中分享过某篇文章」,我之所以能自然地在写作时想起某篇文章、某期播客或某个视频,主要的原因就是我认真读过,并且用自己的文字阐述过它,更重要的是,它引发过我的思考。
不能排除这样一种情况:我读完了一篇文章、看了一期视频、听完一期播客,但是内心毫无波澜,我就算能总结出内容,也表达不出任何观点。这并不是身为读者自己的问题,很大的可能是,自己和作者的思维方式、生活经验、知识储备和背景都差异太大,对不上电波,或者这篇内容的主题自身本来就不感兴趣。标题是很容易蛊惑人的,尤其是在这个争夺流量的互联网环境里,标题吸引人不代表自己应该读下去。如果读了一半觉得没意思,那就不要强迫自己读完;如果读完了觉得跟没读一样,那就当作没读。在我看来,「我明明都读了,不写下来证明自己读过怎么行?」的思想是非常愚蠢的,自己努力过不代表自己真的产出了任何有用的结果。实际上,我觉得「想要证明自己做过一件事情」的心态就是「自己没做成什么有意义的事情、没产出有价值的结果」这一事实的侧面体现。在一篇无聊且对自己无用的文章上花费的时间精力,已经是沉没成本了,一个理性的人应该忽略它。
我在英文博客《 How I Simplify My Reading Workflow 》中提及了一个我延续至今的习惯:
Delete articles that stay for too long.
删掉那些逗留太久的文章。
原因是,如果一篇文章在你的阅读列表或者稍后读应用中停留了太久,纵使你无数次生起读它的念头,但每次都没有打开,或者没能坚持读完,那么,你就应该毫不犹豫地删掉这条链接。如果你真的想读,那你早就读了。我在 第 55 期周刊 中分享了 Michael Nielsen 的文章《 How I use memory systems 》,他提到了写记忆卡片的一个误区是「添加那些你觉得你应该感兴趣的东西」而不是「你真正感兴趣的东西」。我认为这是任何阅读的大忌,除非你需要用它来通过考试或维持生计,如果是为了自我提升或获得智识上的愉悦感,那你应该去读、写那些你真正感兴趣的内容,没有什么东西是你本来就应该学的。
如果你真的改不掉「我必须证明自己读过」的习惯,那就发到社交媒体上,毕竟那上面到处都是这么做的人。
另一个误区是,我认为你不应该过多地参考别人的周刊结构。就像是开始用 Emacs 或 Vim 编辑器时,直接使用别人的配置一样(如果你真的懒得配置,那为什么不去用 VS Code 呢?),你的习惯可能和别人不一样,使用别人的系统可能会给自己带来不必要的阻力。这也是为什么我在一开始就建议读者,花些时间察觉自己每天接收的信息和对应的信息源,再自己断夺哪些内容应该写在周刊里。
以上的所有建议,除了适用于文章,也都适用于视频、播客以及任何其他的你能想到的信息媒介。我见过有人会在他们的周报里分享他们这周看过的印象深刻的短视频,尽管我有些微词,但这也比只看不思考的消费行为好太多了。
对我而言,期刊是一种令人感到亲切的文字媒介,就像追剧一样,你很清楚新内容会在每周的固定时间出现,你和作者都会信守承诺。知道你会定期出现的读者更容易留下来,几个月前我因为前一天熬夜的原因,周刊直到中午才发出去,早上的时候就有一位读者问我什么时候会发布这周的周刊,被催更其实是一件很高兴的事情,这让我知道:有人一直在关注我。更重要地,写周刊是自己对自己的承诺,世界上大概没有比这更神圣的契约了。有人说这是用「输出倒逼输入」的方式,我觉得很准确,因为知道自己在每周的固定时间一定要发布有一定内容量的刊物,所以在截止日期到来之前就会不断地往里面填充内容,为了填充内容,自己当然会去认真阅读更多的信息。
诚然,这种方法不适用于所有人。对一些人而言,强迫自己在固定时间内读完一定量的文章可能会带来压力,如果产生了赶稿的心态,就可能导致质量下滑,读得也不那么认真,就又变成消费行为了,得不偿失。每个人的情况不同,不定刊也是很好的做法,只要符合自己的需求就好。 Taxodium 的 Zine 就是不定时发布的,在我看来这有点像一个缓冲区(buffer),等缓冲区写满了再发出,以内容量为单位而不是以时间为单位。
写作本身就会反过来塑造思维。对我而言,写周刊让我阅读时的心态发生了改变。我以前可能会在读到有意思的句子之后,就会打断阅读过程,找纸笔把它抄下来,或者复制粘贴到别的地方,但在我写周刊几个月过后,我发现自己会更在意「我刚刚读的那几段文字,讲了些什么」,而非「刚刚那一句具体的话,我又没有记住」。要求自己在分享了一篇内容之后阐述自己的思考,自然也会使得自己在摄取信息之后下意识地联想和提炼,这算是一种对思维方式的锻炼。
除了整理信息流,也有不少人(包括我)会在周刊里分享近况(对我而言,是取代 Now 页面 的功能,详见《 为什么我不写 Now 页面 》),这也能起到「梳理生活」的作用。从我的经验来看,把任何东西用结构化的方式表达出来,无论是写作还是画图,都会让思绪更加清晰。
此外,我认为周刊这种松散的记录形式对大多数人来说也是更容易坚持的,写下来的都是自己有真切体会的东西,很适合用来开启写作习惯。
2025-12-01 09:23:00

ずっと真夜中でいいのに。
誰も命無駄にしないようにと 君は命に終わり作ったよ
是为了不让任何人虚耗生命 你才为生命设下终点的吗?だから君がいないその時は 僕は息を止め 待つ
因此当你不在身边之时 我便屏息凝神 静静等待
现在听英语歌已经没办法做一个听不懂词的麻瓜只听音乐了,所以偶尔还是会听听日语歌。其实,就算是从网易云音乐转到 Apple Music,我也有一直关注「ずっと真夜中でいいのに。」这个 @Broca 推荐给我的摇滚乐队,总觉得没有难听的歌呢。放在周刊开头的这首是翻唱作品,原唱是 RADWIMPS,原唱也很好听。
🔍这是 2025 年 11 月的最后一期周刊,照例进行本月的文章回顾。
这个月的文章数量有所减少,但字数却超过了十月,原因是这个月写了几篇万字长文。这个月整体的文章质量,我自我感觉都还不错。
本月还发布了一篇十月份发布在 Backrooms 中文维基上的小说《 脸盲症 》,如果你感兴趣的话可以读一读。
📜
📜
作者的父亲去世几天后,他们在遗物里找到了很多情书,其中有一条是这样的:
i love dota and i love peaches, but i love you more. i will quit smoking and lose weight for you. the happiest days of my life are the ones that start with you across the breakfast table from me.
我爱玩刀塔爱吃桃,但我更爱你。我会为了你戒烟和减肥。我生命中最快乐的日子,就是以和你面对面吃早餐开始的日子。
这写信不是写给作者母亲的,作者的父母分别在 27 岁和 26 岁的时候,因为年龄太大,被父母逼婚才走到了一起。父亲与母亲和弟弟关系都很差,经常连续几个月甚至一整年都在外地工作,最近还去了加拿大的一个城市。正因为这种距离,作者对父亲唯一的亲子记忆,就是她七岁时,父亲在床边给发烧的她讲故事;但作者还是和父亲有种独特的亲近,他们以前会一起散步很长时间,在几十分钟的沉默之后,父亲会跟她分享他的悲伤和对生活的失望。作者从没觉得父亲真正幸福、为自己活过。
可是,很快作者遇到了她父亲的情人,他们认识了三年,在一年半之后在一起了1。情人表示他们相遇时,两个人都感受到了前所未有的感觉,父亲说服他搬来加拿大和他一起住,所以三十多岁的情人为了他只身来到异国他乡。在情人眼里,他们像是订婚了一样,父亲似乎说过他迟早会出柜2,和母亲离婚,然后开诚布公地和他度过余生。当作者看到情人给她的照片时,他几乎不敢相信照片上那个洋溢着笑脸的幸福的男人,和在家里恍惚地坐在电视机前的父亲,是同一个人。
读到这里,我真的挤出了几滴眼泪,和作者自己一样,我本以为这是一个在恐怖的社会偏见下挣扎追求幸福的故事。作者「如果父亲还健在,我可以去他和他情人的真正的家里拜访,见证父亲真正幸福的模样」的美好幻想还没开始就结束了。
because you see, my dad was a coward. mom had started asking for divorces by the time i was in my teens, and dad was the one who always said no. he would complain to her mother, a traditionalist, to ensure that she would berate her daughter back into line. his family and his culture had no place for him, so he used her as a shield to make sure that he would be spared the scrutiny. slowly, we found evidence of other affairs, going back decades.
因为,你看,我的爸爸是个懦夫。妈妈在我只有十几岁的时候就开始要求离婚,我爸爸才是那个一直不同意的人。他会向丈母娘,一个恪守传统的人,抱怨这件事,来确保她会训斥她的女儿让她守规矩。他的家庭和文化容不下他,所以他把母亲当做盾牌,来保证他不会受到审视。慢慢地,我们找到了其他婚外情的证据,可以追溯到几十年前。
情人说,父亲大学时就知道他喜欢男人了,他在柜子里面藏了 40 年——作者写道,她想到这个数字都觉得有些幽闭恐惧症,只有 20 岁的我也无法想象这样活着是什么滋味。
he wasted his entire life, my mom said to me, the evening we found the love letters. his entire life, and mine as well.
“他浪费了他的整个人生”,我妈妈跟我说,就在我们找到情书的那天晚上,“他的整个人生,还有我的人生。”
最终,他们找到了 14 起婚外情的证据,谁知道还有没有别的。要和父亲共度余生的情人是第 11 号。结果,父亲一直跟所有爱他的人隐瞒自己的真实生活——他的父母、兄弟姐妹、他的家庭,还有他的那个情人。
和现在的室友开始合租之前,他借给我一本漫画看,名字叫《暗流》,里面就有一个一声不吭就抛妻弃子离开的丈夫,漫画聚焦的就是这个妻子的故事。我忘记情节了,因为我没有很喜欢那本漫画,但室友很喜欢。我们在聊天的时候,室友说他展示在别人面前的自我可能只有不到 50%,甚至 80% 都是藏起来的,他还觉得大家都这样;他说漫画里的妻子并不了解他的丈夫,而且他并不觉得丈夫的举动有什么问题。
或许我不能谴责那些善于伪装和立人设的人,但我可以肯定他们活得没那么自在——明明过好一个人生就很累了。那部漫画里的妻子或许是不幸的,但她的确把自己的人生托付给了一个虚假的人,他爱的从来不是那个丈夫,而是那个丈夫展现在她面前的一个假人,这的确是她自己要为选择而承担的代价;但在我看来,她是勇敢的,她没有通过不诚来逃避身而为人的选择。
这两篇文章的作者没有批评社会,没有批评那些传统且保守的人,她批评的是她的父亲:
because you see, my dad was a coward
因为,你看,我的爸爸是个懦夫。
#存在主义 #性少数
📜
作者观察到,最近有不少人选择重新开始写个人博客,以抵抗社交媒体。作者回忆起十多年前的 Web 生态和博客圈,除了不限话题、内容繁杂的个人博客,还有一类专注于特定话题、高质量、信息来源可靠的博客,被称作 niche blog(下文称作「专业博客」)。这类博客质量很高,而且能作为可靠的信息来源,能在不牺牲读者体验的情况下盈利(和那种还没来得及开始读就弹出窗口请读者订阅 Newsletter 的博客不算)。作者举例说 Problogger 就是这样一个网站,从 2004 就有了,是一个教人怎么写博客的专业博客;评论区还有人自荐 ResearchBuzz 这个网站,从 1998 年到现在一直在坚持更新数据库、搜索引擎和在线信息集合相关的内容。
作者好奇,既然个人博客回归了,那这类专业博客是不是也应该回归?我们似乎没有别的选择:难道要从遍布社交媒体的错误信息里面找答案吗?难道要忍受在搜索引擎里查询内容时 AI 生成的总结吗?难道 Web 只能被 AI 生成的垃圾填满吗?在这样的背景下,我们可能比以往更需要专业博客,作为可靠的信息来源。
作者认为个人网站的东山再起是重建一个兴盛的 Web 的第一步,接下来人们应该关注如何重建那些向所有人开放的、可信的信息源。
我知道一个叫做 Wait but Why 的博客,或许算不上严格意义的「专注某一话题」的专业博客,但这个博客的确比较出类拔萃,作者在某一篇文章里还称「自己已经把 Wait but Why 作为自己的事业了」(不过最近的更新评论、热度和质量似乎有略微的下滑)。在国外,人们或许更愿意为这样具有特色、有深度,同时也具有个人色彩的内容付费,国内就不好说了。
无论如何,在短视频横行霸道、社交媒体把互联网变得越来越极端、大语言模型让人们逐渐放弃思考的当下,专注写高质量的博客内容,对作者、读者和 Web 环境来说都有极大的好处。
#互联网文化 #写作
📜
作者阅读了 16 世纪法国作家 Michel de Montaigne(米歇尔·德·蒙田)的随笔,产生了一些感想。这个人以《随笔集》(Essais)这一作品名留后事,基本上发明了随笔(Essay)这一写作形式3。作者在他的一些文章中读到了有趣的观点,并分享了出来。
其中我认为最有意思的是这一条:
Callicles 在《柏拉图》中说:极端情况下,哲学是有害的。他建议我们,超过了有益的界限之后,就不要再深入:适度的摄取哲学是愉悦且有用的,但最终可能会导致人的邪恶和野蛮,轻蔑宗教和现有法律,成为社会交流的敌人,人类愉悦的敌人,对统治城市、帮助人他人,甚至帮助他自己都毫无用处——对于这种人,你可以毫无顾忌地扇他耳光。
这和互联网俚语「touch grass」有相似之处,这个俚语的意思是:别宅在家里玩网了,出去摸摸草,接触真实的世界吧。
其次是这条:
我喜欢强烈、亲密、有男子气概的友谊,那种在锋锐且剧烈的交流中获得喜悦的友谊,就像爱情在见血的撕咬和抓挠中获得欢愉一样。
作者自认为是幸运的,因为他生活在一个已经被蒙田的作品改变过的世界里。他打下了基石,使得活在现代的他可以无需解释为什么人们应该欢迎反对意见、欢迎被证明是错的,就和朋友展开有趣的讨论——我真希望我也能说同样的话,至少我现在还没有找到太多在我提出反对意见时,不会被情绪劫持、不会逃避争执、愿意承认自己被说服的人。
#认知
作者发现旧金山轻轨上显示目的地的显示屏别具一格,他研究并模仿了这种显示屏显示字母的方式,把它们做成了一款字体。

访问: Fran Sans Essay — Emily Sneddon
开源版的 Figma。
访问: GitHub - penpot/penpot: Penpot: The open-source design tool for design and code collaboration
一个允许用户编写 Lua 脚本控制 macOS 系统,以实现强大自动化的 App,开源免费,非常黑客。我发现这个 App 是因为最近改变了自己使用设备的逻辑。以前我出门会用 MacBook,在家就用桌面上的 Mac mini,但我逐渐发现在两台电脑之间同步数据非常痛苦,时常忘记 git pull,一些软件的配置项更难同步,再加上两台电脑上装的软件不一样,维护一个整洁的 $HOME 目录就已经很要命了,如果用同样的态度处理两台电脑,我大概会在扯不开的毛线堆里疯掉的。所以,现在我的思路是,需要敲代码和写文是,就用 MacBook,在家的话可以连接外界显示屏;而 Mac mini 就作为一台长期开机的 Home Server,同时起到娱乐功能(看剧,以及…… 用 Mac 打游戏)。
这样区分之后,MacBook 就经常需要连接外接显示器。在较小的屏幕上,我需要 Dock 栏自动隐藏,并且显示在最底部;但是在较大的外界显示屏上,Dock 栏最好是一直显示,而且放在左边会更好。然而,macOS 没有提供分显示器的 Dock 栏设置,于是我只能通过 Hammerspoon 这样的脚本工具来监听外接设备连接情况,然后自动更改设置。
因为只是想临时解决一个小需求,我就没有去看 Hammerspoon 的文档,而是让 ChatGPT 帮我写了一个脚本。如果以后有更黑客的需求,我大概会去读一下 Hammerspoon 提供的 API。
-- 自动检测屏幕变化并调整 Dock 行为
local function updateDockPosition()
local screens = hs.screen.allScreens()
local dockPos = "bottom"
local autoHide = true
local foundExternal = false
-- 查找名为 "P5" 的外接屏
for _, s in ipairs(screens) do
if s:name() == "P5" then
foundExternal = true
break
end
end
if foundExternal then
-- 外接屏存在:Dock 左侧 + 不自动隐藏
dockPos = "left"
autoHide = false
else
-- 无外接屏:Dock 底部 + 自动隐藏
dockPos = "bottom"
autoHide = true
end
-- 设置 Dock 位置 & 自动隐藏
local cmd = string.format(
'defaults write com.apple.dock orientation %s; ' ..
'defaults write com.apple.dock autohide -bool %s; ' ..
'killall Dock',
dockPos,
autoHide and "true" or "false"
)
hs.execute(cmd)
end
-- 监听屏幕改变事件
local screenWatcher = hs.screen.watcher.new(updateDockPosition)
screenWatcher:start()
-- Hammerspoon 启动时执行一次
updateDockPosition()
访问: Hammerspoon
🎉恭喜你发现了新栏目。
前两期周刊都有在「↯ · 当下」这个栏目里介绍「这周看了什么」,既然每周都想写,那不如就单独拿出的做一个栏目,这一周看的剧、音乐剧、电影之类的都会放在这里——当然也可能没有。
早早地就在日历上标注了 11 月 27 日开播的时间,但直到 28 日晚上我才想起来,于是踩着 11 月的尾巴把《怪奇物语》第五季的前四集看完了。这次的播出方式是 11 月底放出前半季,圣诞节放出第 5、6、7 集,在跨年的时间放出最终集——也就是说观众还要等一个月才能看到后续的剧情,混蛋啊!
第五集每一集都接近一个小时,而且,由于前四季积累起来了太多的人物线和故事线,在这一季里所有的人物都分成小组行动、探寻真相,人物有各自的支线任务和信息差,即使是这样剧情也没有乱成一坨毛线,推进得很紧凑,观感很好,总之是非常精彩的一季。
不过,第四集被绑起来的 Derek 一家人还被关在谷仓里啊!你们都不管了吗!为什么我要等一个月才能知道他们到底要怎么给这一票绑架收场啊!
《同乐者》第五集的推进还算有趣,这里讲一个小点:在第五集的最后,Carol 发现了同乐者们一直在饮用的褐黄色液体的原材料,露出了震惊的表情,很多观众都在猜测 Carol 看到的是什么。
一般人看到有人露出这种震惊的表情,应该都会猜测袋子下面是人肉吧。在这一集前面,有一段情节是 Carol 到牛奶工厂(实际上是生产这种褐黄色液体的工厂),看到有一群乌鸦在啄袋子,而乌鸦就是食腐动物;在前面还有郊狼刨坟的情节,虽然没有直接关联,但应该是一种暗示。有人指出褐黄色液体应该是「血粉蛋白胨」,颜色一致。同乐者在同化人类的时候因为意外,死掉了很多人,而 Carol 因为发脾气也让同乐者失控过,死掉了两批人;第二集的开头,有牛奶公司的卡车运输尸体的情节——所以基本可以肯定,同乐者一直在饮用死掉的人类同胞的尸体制成的液体。
继 wthis 这个命令行工具之后,我开始探索 Go 语言的 Web 开发能力,开启了 growel 这个项目的开发。Growel 的定位是用于构建轻量级 Web 后端的框架,只通过 JSON 进行数据交换而不使用模板语言,可以快速开发 API。目前大概是做出来了一个可以这么用的东西:
api := growel.New()
api.GET("/hello", func(c *growel.Context) {
c.JSON(200, map[string]string{
"message": "Hello, Growel!"
})
})
api.GET("/user/", func(c *growel.Context) {
c.JSON(200, Users)
})
api.GET("/user/:uid", func(c *growel.Context) {
uid, err := strconv.Atoi(c.Params["uid"])
if err != nil {
c.BadRequest("Invalid user ID")
return
}
for _, user := range Users {
if user.ID == uid {
c.JSON(200, user)
return
}
}
c.NotFound("User not found")
})
api.Start()
谁敢想用 Java 写这样的东西会会写几个 Controller 类和 Service 类。
其实 Go 标准库提供的 HTTP 相关的方法就已经很好用了,而且 JSON 解析也非常丝滑,所以做一个 Web 框架非常简单,只需要封装一些方法、添加合适且必要的抽象层,让 Web 开发体验变得更丝滑就好了。
开发 Growel 的时候我才意识到,自己在学校里学了快一年的 Java Web 开发,一直在用 Spring 框架,连 Web 程序是怎么启动的都不知道,完全被 Java 生态系统里无处不在的「魔法」变成麻瓜了。虽然 Go 的 Web 框架已经足够多了,但重复造轮子其实是很好的学习方式,手写 Router、封装 http.ResponseWriter 和 http.Request、自己实现动态路由都让我学到了不少。
把原先的「议叙」(post)拆成了两部分:议论和散文。
你也可以理解为,我把文章分成了 T 和 F 两部分(仔细想想,其实更像是 J 和 P 的两部分,判断和感知的区分)。做这个拆分是因为,我发现自己的 T 和 F 可能会在某个时间段宕机其中一个,把这个分开能让我更安心地在情绪崩溃时发疯、在好奇心超负荷运转时写一写没人关心的新话题。
迁移到「议论集」分区的文章,URL 会从 /posts/* 变成 /essays/*,但都有做重定向,原先的链接还是能够访问,原先的 posts 变成「散文集」,URL 不变。
周日从学校回家时心情很差,于是步行去家附近的零食店。倒不是有多想吃东西,只是想顺便找个理由逛逛。作为处女座特质很强的人,在物资充足、商品整齐排列的地方待着怎么会心情不好呢?只不过,这一路上可不好走,路边的建筑工地一直在喷水降尘,大概是因为刚好在下风向,我必须穿过混杂着化工气味的水雾,而且这条路没有分出人行道和骑行道,摩托车电瓶车在这个时间点又开得飞快。不过,就算有专门的骑行道用处也不大,走到一条商铺比较多的街区时,到处都是外卖骑手在穿梭。

我很喜欢步行,去一个地方的时候只要能在 30 分钟内走到,我都会步行。我主要是喜欢走路时放空脑袋或者听听音乐、博客的感觉,走在路上的时候不需要在意任何事情;另一个附加的好处是可以锻炼身体,我走路比一般人快很多,尽管也达不到高效的锻炼效果,但每天走三四公里的路的确能消耗很多热量(以至于我在放假不需要步行时,都会悄悄地发胖)。
只不过,在这样一个大家都在赶路的城市时,这种愉悦也是奢求了。那天晚上回到家之后我心情很差,因为路上一直在避让该死的电瓶车和摩托车,没有办法匀速走动,让我本来就混乱的思绪都绞在了一起。我想不明白,即使是在公园里,为什么也会有摩托车在跑步道上开。
2025-11-25 23:49:00
从这个月 10 号,水逆期开始,我就开始减少出门,宅在家里读一读书,捡一捡法语学习,看看音乐剧,还找了些新东西来学,Go 语言就是其中一个。一开始只是抱着好奇心探索,并没有什么功利心,不过越学越发觉,Go 真的很适合用来写高并发高性能的 Web 应用(而且越学越讨厌 Java),也完全能胜任系统开发,就业岗位貌似不少,竞争也相对更小。更重要的是,我非常喜欢这种「观点鲜明」的编程语言,我想我会一直用下去的。
学任何一门编程语言我都更喜欢「不自量力」地直接上手写代码,一边写一边学基础语法,遇到不知道怎么实现的需求再查阅文档。不过,学 Go 的一开始我发现了 Go Proverbs 这个网站,便在实战之外还花了不少时间研读这里的「名言警句」。这些 Go 语言编程相关的谚语大多来自 Rob Pike 在 2015 年的 一场演讲 ,每一条都很有启发性,这篇文章就来逐条阐述我对它们的理解,其中有不少也可以用在 Go 语言以外的编程中,希望能引发读者的一些思考。
这句话直接翻译过来是:不要用共享内存的方式通信,要用通信的方式共享内存。
这需要一些上下文来理解。Go 是一门可以通过非常优雅的方式实现并发的编程语言,异步编程若是要在多个线程之间同步信息,往往需要加锁,这是为了避免线程同时操作内存,导致冲突、数据准确性(integrity)被破坏等无法预测的问题。加锁一般指的是互斥锁(mutual exclusion object,简写作 mutex),线程操作数据时需要先获取对应的锁,如果锁被其他线程持有,则需要等待,获取到锁的线程才能执行操作,这保证数据操作是互斥的。
加锁是异步编程中的常见做法1,但「锁是痛苦的」,不仅要创建锁、管理锁,还要想办法避免死锁等运行时错误,这类错误没法被编译器察觉,程序员排查起来很困难。所以,为了保证程序的健壮性和可读性,应当避免使用锁。线程之间之所以要用锁来实现互斥,是因为共享了同一块内存区域,而共享内存是为了传递数据,也就是这句话的前半句「communicate by sharing memory」。共享了内存就必须加锁,不然就会出现问题,而锁又会带来更多难题。
Go 语言鼓励的「share memory by communicating」,要用到 Go 语言的一个特性—— channel。你可以把 channel 理解为在不同线程之间传递数据的「通道」。以下是一个整数类型的 channel 例子。
intChan := make(chan int)
// 线程 A 传入一个数据
intChan <- 10
// 线程 B 取出这个数据
someVar := <- intChan
严格来说,这两个线程仍然共享了 intChan 这个 channel 类型的变量,但整体上,这种写法避免了直接读写被储存在内存中的数据。在线程 A 没有传入数据时,线程 B 执行到 <- intChan 这一行就会自动阻塞,直到取到 channel 中的数据。在 channel 的抽象下,线程不再操作一块无人管理的,需要线程自觉加锁、等待用锁的数据区域,而是相互之间直接传递数据——这就是「share memory by communicating」。
Go 语言实现并发的方式非常优雅,而 channel 的存在又简化了线程之间共享数据的方式,这使得用 Go 编写并发程序的体验非常舒服,这一点在接下来还会看到。
这句话直接翻译过来是:并发不是并行。
这其实是计算机科学的基础知识,并发(concurrency)和并行(parallelism)本来就不是一回事。一般来说,并发指的是程序在处理器上交替执行,在单核处理器上,一个进程完全占用 CPU 资源,完成计算或者用完时间片之后,其运行所需的上下文会被保存,然后 CPU 进行上下文切换(context switch),换上下一个要执行的进程的上下文,然后运行下一个进程。每个进程执行时都完全占用一个处理器内核,时间到了之后就让下一个进程执行,只不过进程在 CPU 上切换的速度非常快,宏观上让人感觉是在同步执行。并行指的是真正的同步执行,在多核处理器上,不同的进程在不同的处理器内核上执行,而不需要交替执行。
Go 语言是跨平台的,不需要考虑硬件底层,也就是说,编程语言不关心执行程序的计算机究竟有多少个处理器核心。Go 语言强调并发并非并行的原因是,Go 把并发当作一种「编程结构」,即组织程序的方式。程序员在进行多线程编程时,应该聚焦于程序结构而非硬件底层,如果关注程序结构,就能写出更清晰、可读、可维护的多线程程序代码;而且,也不能理所应当地认为自己用了并发的结构,程序就一定是并行的。
Go 语言里实现并发非常简单,假设我们有一个 FetchData() 函数,而这个函数需要进行大量 IO 操作或者需要发送 HTTP 请求,如果同步执行会长时间阻塞主线程,要把这个函数变成并发的,只需要在调用函数时,在前面加一个关键词 go。这有点像 JavaScript 里的 async,但并不相同。
go FetchData() // 这样就创建了一个线程执行 FetchData()
在 Go 语言里,一个线程被称作 goroutine,和 Java 中 Thread 的显著区别是:Thread 是一个类,需要创建和管理对象;goroutine 是一种控制结构,直接写就好了。另一个很重要的区别是,Java 线程占用的内存是 MB 级别的,而一个最基本的 goroutine 大小只有 2 KB。简单来说,goroutine 用起来很简单,创建的成本也很低。
很重要的一个点是,将并发作为一种控制结构也让代码变得更可读了,比如上面那一行代码,就可以很有喜感地读成:Go! Fetch data!
以下是一个 goroutine 配合 channel 的并发示例,代码来自我的项目
wthis
:
fmlChan := make(chan *FormulaInfo)
caskChan := make(chan *CaskInfo)
rvsChan := make(chan []string)
// start 2 goroutines, fetching formula/cask info and uses
go func() {
formula, cask := GetBrewInfo(pkgName)
fmlChan <- formula
caskChan <- cask
}()
go func() {
rvsChan <- GetBrewUses(pkgName)
}()
stat = NewStatistics(<-fmlChan, <-caskChan, pkgName, <-rvsChan)
总结一下,关注程序结构,写出更清晰的代码,在逻辑结构上让代码异步执行,这是并发;至于并行,那与硬件底层和操作系统有关,不能认为按并发结构编写的代码就一定是并行的。
这句话直接翻译过来是:Channels 编排;互斥锁串行。
尽管 Go 提供了在大部分时候都更好用的 channel,也更鼓励使用 channel,但互斥锁并不是被禁止的,也没有从这门语言里消失,因为有些时候必须要用到锁。这句话简单回答了「什么时候该用 channel?」「什么时候该用锁?」这两个问题,也能帮助 Go 程序员更好地理解 channel 的用途。
编排这个词很有意思,它的原文是 orchestration,又名 choreography,意思分别是「编配管弦乐曲」和「编舞」。如果你理解概念,就会觉得这个描述非常生动形象。这个词在不同的语境下的意思有一些区别,但一般指代一种自动化的工作流,可以统一调度、掌控多个作业,高效处理大量用户请求,或者让多个程序之间相互协作等等。在管弦乐(orchestration)中,各个演奏者各司其职,但由指挥家统一主导;在舞蹈(choreography)中,舞者之间需要相互沟通,分工协作。这引申到编程中,对应了不同的编程结构,下面用简单的例子来解释。
假设有一项操作需要大量计算,这些计算被分散开,启动多个 goroutines 执行,最后由主线程将计算的结果整合起来,Go 语言代码可以这样写:
// 有 100 项作业需要执行,相应地有 100 项结果需要接收
// 分别创建 channel
jobs := make(chan int, 100)
results := make(chan int, 100)
// 启动 10 个 Worker 执行这些作业
for w := range 10 {
go Worker(w, jobs, results)
}
// 传入 100 个作业给 jobs channel
// worker 接收到之后就会执行
// 执行完一个作业之后会继续接收 jobs channel 里的剩余作业
for j := range 100 {
jobs <- j
}
close(jobs)
// 收集 100 个作业结果
for _ := range 100 {
doSomethingWith(<- results)
}
这是一个典型的 Worker Pool。这个例子中,主线程就是指挥,而 Workers 就是乐队成员,整体上,这就是一种编排(orchestration)。
假设有三个作业(A、B、C),C 作业依赖 A 和 B 的结果,而 B 依赖 A 的结果,要让这三个作业相互配合完成工作,可以这样写:
a := make(chan int)
b := make(chan int)
c := make(chan int)
// A
go func() {
result := doSomething()
a <- result
}()
// B
go func() {
result := doSomething(<-a)
b <- result
}()
// C
go func() {
result := doSomething(<-a, <-b)
c <- result
}()
三个作业各司其职,使用 channel 相互通信完成了工作,A、B 和 C 都可以被视作相互配合的舞者,这也是一种编排(choreography)。
Go 语言还提供 select 控制结构,用于监听和操作 channel。在线程等待多个操作返回结果的时候,可以使用 select,数据先从哪个管道过来,就先处理哪一个。
// 例子来源:https://gobyexample.com/select
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
for range 2 {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
select 和 channel 为 Go 语言提供了十分强大的编排能力,允许程序员在大多数情况下高效处理业务,而不是机械地等待上一个操作完成再做下一个。当然,严格的串行也是存在的,通常是为了保证某一操作的原子性(Atomic),即不可被打断性,这个时候就要用到锁。
在 Go 中使用锁需要用到标准库 sync,一个常见的做法是把 sync.Mutex 放到使用它的结构体中。以下例子实现了一个计数器容器,一个 Container 中有多个计数器(counter),inc(自增)方法用于自增指定计数器。默认情况下 ++ 是一个非原子(non-atomic)操作,如果多个 goroutine 都在对同一个计数器自增,极有可能会相互影响——例如,A 读取到计数器的值是 5,自增后写回 6;B 在 A 写回之前也读取了计数器,值也是 5,也写回 6,这就覆盖了 A 的操作;两次自增的结果本应该是 7,但由于操作的非原子性,变成了错误的 6。
为了保证自增操作的原子性,可以在自增时加锁,不允许其他 goroutine 读写。
// 例子来源:https://gobyexample.com/mutexes
type Container struct {
mu sync.Mutex
counters map[string]int
}
func (c *Container) inc(name string) {
c.mu.Lock()
defer c.mu.Unlock()
c.counters[name]++
}
// ...
注意 defer c.mu.Unlock() 这一行代码。defer 关键词表示这行代码会在其周边代码执行完毕之后被执行,用在 Unlock() 和 Close() 这样的操作前面,就可以把它们紧挨着 Lock() 和 Open(),防止忘记关锁。defer 还会在程序 panic(遇到无法处理的错误)时执行,因此 defer 也经常被用来恢复错误。
以上操作也可以直接用 Go 标准库提供的原子计数器,不需要自己封装。
var ops atomic.Uint64
ops.Add(1)
总而言之,channel 用于高效、灵活地编排程序,而 mutex(互斥锁)用于安全、严格地保证串行。
这句话直接翻译过来是:接口越大,抽象越弱。
熟悉面向对象编程的程序员应该不会对 interface(接口)感到陌生,它用于规定一个类应该实现什么方法,也让静态类型语言的类型系统变得更强大。例如,Java 中的 List 接口规定了列表应该实现 add() 等方法,同时也让底层实现不同的列表(ArrayList、LinkedList…)可以通用——如果一个方法要求传入一个 List 作为参数,无论传入 ArrayList 还是 LinkedList 都是可以接受的。
Go 语言鼓励程序员编写「小巧」的接口,就像这样:
type Writer interface {
Write()
}
只有一两个方法的接口看起来很没用,但语义非常明确:Writers write; Dancers dance; Eaters eat. 用 -er 后缀结尾的词命名接口几乎是 Go 语言的惯例。小巧的抽象是强大的,它能让程序员更灵活地使用 Go 语言的类型系统。
例如,这是一个日志接口:
type Logger interface {
Log()
}
在某个系统中,日志可能有三种输出方式:直接输出到终端、写入文件和发送 HTTP 请求给远程服务器。
type StdoutLogger struct{}
func (s StdoutLogger) Log(msg string) {
fmt.Println(msg)
}
type FileLogger struct {
f *os.File
}
func (fl FileLogger) Log(msg string) {
fmt.Fprintln(fl.f, msg)
}
type HttpLogger struct {
endpoint string
}
func (h HttpLogger) Log(msg string) {
http.Post(h.endpoint, "text/plain", strings.NewReader(msg))
}
现在,我们正常编写业务逻辑,在处理某个数据的时候,突然有了记录日志的需求。由于我们定义了 Logger 接口,处理数据时不需要思考应该用哪种方式输出日志,只需要接收一个 Logger,直接 Log() 就好。调用这个函数时再传入具体的 StdoutLogger FileLogger 或者 HttpLogger。
func Process(data string, logger Logger) {
logger.Log("processing: " + data)
}
短小的接口看起来没用,但接口完全可以像搭积木一样把方法堆叠起来,比如下面这个类型就既是 Writer 又是 Closer 还是 Logger。
type FileWriter struct {
f *os.File
}
func (*fw FileWriter) Write() { ... }
func (*fw FileWriter) Close() { ... }
func (*fw FileWriter) Log() { ... }
interface 实际上是对类型的「限制」,规定的方法越多,接口越大,要实现这个接口就越难,应用的场景也就越局限。Go 语言不是严格意义上的面向对象编程语言,它更关注类型,而短小的接口使得程序员可以更灵活地使用类型系统。
顺带一提,和面向对象编程语言不同,Go 语言不需要显式地指定实现哪些接口,只要包含了接口规定的方法的类型,都会被归为这个接口。
// 在 Java 中实现接口
class FileWriter implements Writer {
public void write() { ... }
}
// 在 Go 中实现接口
type FileWriter struct { ... }
func (*fw FileWriter) Write() { ... }
这句话直接翻译过来是:让零值变得有用。
Zero value 指的是变量被声明但尚未初始化(赋值),类型系统自动为变量填充的值。对字符串来说,这个值是 "",整数是 0,浮点数是 0.0,布尔值是 false,指针是 nil。结构体被声明时,其中的变量也会被填充零值。
Go 语言鼓励程序员好好利用这个零值,让类型即使没有初始化也能正常使用。标准库里有很多例子,前面提到的 sync.Mutex 就是一个。让我们回看这个例子:
// 例子来源:https://gobyexample.com/mutexes
type Container struct {
mu sync.Mutex
counters map[string]int
}
func (c *Container) inc(name string) {
c.mu.Lock()
defer c.mu.Unlock()
c.counters[name]++
}
// ...
显然,inc() 方法没有初始化 c.mu 这个 sync.Mutex 类型的变量,c.mu 里面装的是默认的零值,但零值仍然可以被正常使用。我觉得这句谚语更准确的说法应该是:Make the zero value usable.
另一个例子是标准库里的 bytes.Buffer:
var b bytes.Buffer
b.Write([]buffer("Hello World"))
io.Copy(os.Stdout, &b)
字节缓冲区没有初始化,仅仅是声明过后就可以直接调用 Write() 方法使用。如果是在 Java 语言里,你大概需要 new 一个类的实例才行,或者用到别的构造方法,总之做不到声明即用。
Go 语言之所以强调「让零值可用」「声明即可用」,除了让代码更简单易读、编写程序更简单,另一个原因是在 Go 语言里,空指针(nil)可以用来调用未被初始化的类型的方法。假设你有一个名为 Config 的结构体类型,你为它编写了 Path() 方法,你声明一个 Config 之后并没有指定路径,甚至 Config 本身就是空的,Path() 也可以返回一个默认值。
func (c *Config) Path() string {
if c == nil {
return DEFAULT_STRING
}
return filepath.Join(USER_HOME, c.Filename)
}
简单来说,如果你可以直接使用零值,就不要把事情搞得太复杂!
interface{} says nothing这句话直接翻译过来是:空接口说明不了任何东西。
这里说的并不是在编写接口时没有编写任何方法这种明显的错误,而是指在声明类型时使用 interface{}。前面提到,Go 并非是面向对象编程语言,interface 是为类型服务而非对象服务的;在 Go 里面,实现方法不需要显式声明,只需要实现规定的方法就好了——没有方法的空接口,自然能匹配任何类型。
interface{} 常常被用来接收不确定类型的值,很多时候这种操作是必要的,但 Rob Pike 指出,很多人滥用了空接口类型,他说:
You might as well write in Python.
你还不如去写 Python 呢。2
将代码保持在 Go 严格的类型系统中,有助于保持程序的安全性。除此之外,空接口最大的问题是「不够清晰」——清晰的代码可读性是 Go 语言的核心之一。使用空接口类型让函数的输入和行为都变得不可预测,代码更难阅读,而且更难发现 Bug。
在 Rob Pike 这场演讲的几年之后,Go 发布了 1.18 版本,其中添加了 any 类型,是 interface{} 的别名。添加这个别名的主要原因是让代码变得更可读,人们看到 any 就会知道在这里传入任何类型的值都可以,而不熟悉 Go 的人看到 interface{} 可能会觉得困惑。即使是 any,在使用前也要想清楚自己是不是真的没有办法预测数据类型,还是只是为了图省事而牺牲了安全性和可读性。
合理使用 any / interface{} 的例子是标准库里的 fmt.Println() 方法,程序员的确无法预知要打印的是字符串、数字还是别的什么类型。
总之,这条谚语让程序员严格遵守 Go 的类型系统。
这句话直接翻译过来是:gofmt 的样式不是任何人的最爱,但 gofmt 是所有人的最爱。
gofmt(或者 go fmt)是 Go 语言提供的命令行工具,用于格式化 Go 源代码。写完 Go 语言代码之后在项目根目录执行 gofmt -s -w . 就可以一键格式化代码。gofmt 的样式是统一的,没有自定义的空间,这看起来死板,让程序员不能用自己喜欢的风格格式化代码,但好处在于,有一个官方的格式意味着团队之间无需对代码风格做出讨论和决策,也不需要像 JavaScript 开发者一样纠结用 ESLint 还是 Prettier 等第三方代码格式化工具——用官方的 gofmt 就好了!这样一来,开发者就可以把心思放在更重要的事情上。
总而言之,不要纠结这个地方要不要用 Tab、那个地方要不要换行了,你的开发环境里已经有 gofmt 了。
这句话直接翻译过来是:一点点复制比一点点依赖更好。
大部分程序员都在强调代码复用性(reusability),排斥重复造轮子,追捧 DRY(Don’t Repeat Yourself)的理念,这导致许多程序员在编写简单的需求时也会使用大量的第三方依赖库,即使他们只调用其中的一两个方法,即使自己完全可以封装一个三五行代码的函数来实现相同的需求。
Rob Pike 强调:要保持一个小巧干净的依赖树。维护依赖是痛苦的,因为依赖是动态的,如果依赖库有更新、出了问题,使用了这个依赖库的代码往往也需要跟进。这个教训,已经被 JavaScript 程序员吃尽了,JavaScript 庞大的生态意味着几乎没有一个 JavaScript 项目不大量使用依赖库,而一个 JavaScript 项目要是有几个月的时间没有更新,代码很可能就过时甚至用不了了。
所以:
总而言之,头脑清晰的「C-V 工程师」比喜欢尝试各种库的「时髦工程师」写出来的代码更稳定!
这句话直接翻译过来是:Syscall 必须总是被构建标签守护。
syscall 即系统调用,允许程序从用户态访问内核态,像操作系统发出请求,完成需要系统级权限的操作。系统调用很强大,很多情况下也是必要的,尤其是在系统级开发下几乎必不可少,但系统调用的问题是:它不跨平台。
Windows、macOS 以及各种千奇百怪的 Linux 发行版,各自的 syscall 都有很大差异,几乎不能写出通用的代码。要保证全平台通用,就应该用 os 库;但如果必须用 syscall,就要用 Go 编译器的「构建标签」(build tags)。
构建标签实际上就是写在文件开头的一行注释:
// go:build linux
上面这行代码给整个文件添加了 linux 的构建标签,表示只有在 Linux 操作系统下才编译这个文件。构建标签也可以有别的用处,比如软件有 Free 和 Pro 两个版本,Pro 版的功能就可以单独写在几个文件里,给这些文件加上 pro 的构建标签,然后在构建的时候这样编译:
go build -tags pro
# 在编译时手动加上 pro 标签
区分 debug 模式,区分生产环境和开发环境,也可以用到构建标签。
这句话直接翻译过来是:Cgo 必须总是被构建标签守护。
Cgo(读音类似 Seagull)指的是 Go 语言标准库里的 C 包,这个包提供的方法允许程序员在 Go 语言中调用 C 标准库,还可以在注释里写 C 代码并使用在 Go 代码中。
// 例子来源:https://go.dev/wiki/cgo
/*
##include <stdio.h>
##include <stdlib.h>
void myprint(char* s) {
printf("%s\n", s);
}
*/
import "C"
import "unsafe"
func Example() {
cs := C.CString("Hello from stdio\n")
C.myprint(cs)
C.free(unsafe.Pointer(cs))
}
和 syscall 一样,操作系统提供的 C 标准库实际上并非完全相同,C 语言并不跨平台。如果要用 Cgo,就必须使用构建标签声明这段代码是为哪个平台编写的。
这句话直接翻译过来是:Cgo 不是 Go。
这句话强调的是,Go 语言提供性能良好的垃圾回收机制、安全的类型系统、安全的操作内存的方式,编写纯粹的 Go 代码可以保证准确性、稳定性、安全性以及代码可读性。相反,C 语言本身并不是一门安全的语言,需要程序员手动 free() 释放内存,容易出现段错误等等。如果使用 Cgo,就会失去 Go 语言自带的各种安全机制,很容易出现难以排查的问题。
Rob Pike 建议,尽量避免使用 Cgo,你多半不需要用到它。
这句话直接翻译过来是:用了 unsafe 包就没有保障。
在前面使用 Cgo 的例子里,就已经演示过了 Go 标准库提供的 unsafe 包。它的功能很强大,能绕过类型系统直接访问内存、把指针当作整数来用…… 能力越大,责任也越大。和 Cgo 一样,使用 unsafe 就等于抛弃了 Go 提供的安全机制——跑出安全区的样子好像很酷,但你为什么不直接去写 C 呢?明明用 unsafe 还麻烦不少。
Rob Pike 建议避免使用 unsafe,至少使用之前要想想自己是否真的需要用到这么强大又危险的功能,用的时候也要做好可能出错的准备。
这句话直接翻译过来是:聪明比不过清晰。
这解释了 Go 语言为什么移除了三目表达式,原因就是它不够清晰。一般来说,三目表达式其实非常好用,在 C、Java 和 JavaScript 等热门语言中都存在。
// ? 前面是一个布尔值或者布尔表达式
// ? 后面是为 true 的结果
// : 后面是为 false 的结果
console.log(OK ? "It's okay" : "It's not okay")
这看起来还挺清晰的,但 Go 的设计者发现有不少人会用三目表达式来编写令人费解的控制结构。
const someString = isAnimal
? (isElephant ? "I'm an elephant" : "I'm just a dumb animal")
: (CheckIfItIsAPlant()
? "I can't speak. I'm a plant"
: "Fuck, I'm not even a plant?")
三目表达式被滥用过后会大大降低代码可读性,Go 语言鼓励看起来很蠢,但清晰可读的写法。
var someString string
if isAnimal {
if isElephant {
someString = "I'm an elephant"
} else {
someString = "I'm just a dumb animal"
}
} else {
if CheckIfItIsAPlant() {
someString = "I can't speak. I'm a plant"
} else {
someString = "Fuck, I'm not even a plant?"
}
}
还有那些过度复杂的条件表达式,看起来很聪明,但可读性非常差。
// ❌
if thisVar <= thatVar || (isLikeThis && fetchStatus() != status.OK) {
return
}
// ✅
if thisVar <= thatVar {
return
}
statusOK := (fetchStatus() != status.OK)
if isLikeThis && !statusOK {
return
}
更直接、简单、清晰的代码不仅提高可读性,让维护代码变得更容易(这对自己和团队成员都是好事),也变相提高了代码稳定性,因为清晰的代码更容易排查问题。
执拗地写聪明但不清晰的代码有点像某种大男子主义的攀比心,像是「我昨天熬夜到凌晨四点把工作做完了!」,许多人都会为这种刻苦但实际上是耍帅的精神鼓掌,然而,人的认知能力在凌晨四点到六点比喝醉了酒还要差3,而且熬夜对身体的伤害不必多说。
这句话直接翻译过来是:反射永远不清晰。
反射(Reflection)是很多高级语言里都有的机制,用于在程序运行时检视和操作对象。有的信息在程序运行之前无法预知,要在程序中使用这些不可预知的值,就可以用到反射;比如前面提到的空接口,如果函数接收一个空接口类型(任意类型)的值,但需要根据值的类型执行操作,就可以利用反射来获取这个值的类型。
反射也带点 Meta(元)的意味,经常被称作 Metaprogramming(元编程),因为它可以被用来检视程序自身的结构,比如 Java 语言中就可以利用反射机制获取某个类的注解、构造函数、成员变量等等,并把它们当作普通的对象来使用。
在 Go 语言提供的 reflect 包里,大致有以下几种方法:
// 接收一个 interface{} 类型的值,即任意类型的值
func doSomeReflection(i interface{}) {
// 获取 i 的类型
// 一个定义在 main 包里、名为 User 的结构体会返回 main.User
t := reflect.TypeOf(i)
// 获取 i 的底层类型
// 同样一个名为 User 的结构体,会返回 struct
k := reflect.KindOf(i)
// 获取 i 的值
v := reflect.ValueOf(i)
// 如果要获取值的类型,就可以这样写
vk := reflect.ValueOf(i).Kind()
// 如果 i 是一个结构体,可以用这个方法获取它的字段数量
nf := reflect.NumField(i)
// Field() 方法用于获取字段,配合上面的方法可以遍历结构体
for n := 0; n < numberOfFields; n++ {
// 这里会输出该字段的类型和值
fmt.Printf("Type:%T, Value:%#v", value.Field(n), value.Field(n))
// 这里输出的是该字段的底层类型
fmt.Println("Kind is ", value.Field(i).Kind())
}
}
至于 Rob Pike 为什么说「反射不够清晰」,我想读完上面的代码已经不言自明了,反射就是很难读。我认为最根本的原因是,在代码中引入反射,迫使程序员用另一个完全不同的视角(元视角)来审视代码,这种视角的转换是很困难的,而且往往要同时使用常规的编程逻辑和元编程逻辑才能读懂反射。
这并不是说反射不好,有的需求必须要用反射实现。只不过,清晰永远比聪明更好,如果有更清晰的、不涉及反射的方法,那就不要用反射。
这句话直接翻译过来是:错误是值。
接下来我们正式进入到 Go 的错误处理哲学。Go 语言完全抛弃了传统的 try ... catch 结构,将错误简化为值,将错误处理从一种特殊的控制接口降级为简单的 if ... else。Go 还支持多函数返回值,也就是说,你可以将错误当成一个返回值返回给调用者,而不需要像 Java 一样抛出(throw)。
在 Go 的错误处理中,程序员最常写的就是这样一段代码:
result, err := GetSomeData()
if err != nil {
// 在这里处理错误
}
如果是 Java 等依赖 try ... catch 进行错误(异常)处理的语言,你需要这样写。
try {
// 其他相关的代码
ResultType result = GetSomeData()
// 继续其他相关的代码
// ...
} catch (IOException e) {
// 处理 IO 异常
} catch (Exception e) {
// 处理其他类型的异常
}
这意味着你需要把可能会抛出错误(异常)的代码提前包裹在 try ... catch 块中,而且你还创建一个新的作用域(scope),如果你在 try ... catch 里面获取了数据,就得直接在里面处理这个数据,或者在这个作用域外面先声明函数,再在里面使用。总之是有点麻烦的。更不用说,在 Java 里面你还需要考虑不同类型的异常,光是想到这些我都不想做错误处理了。
Go 语言将错误作为值,不仅让错误处理变得简单直观,减少了一种控制结构,还让程序变得更灵活——既然错误是值,那你就可以把错误储存起来,像普通的值一样直接写进日志里,放进一个 map 里,缓存起来,或者等完成一大批操作之后统一反馈给用户,甚至发送给你的妈妈看——最后一条是 Rob Pike 在演讲时开的玩笑。其他语言也可以做到,但在 Go 里面会更容易,更重要的是,你可以对错误进行编程,而不会被局限在 try ... catch 这一种控制结构里。
总之,错误在 Go 语言里是值,你可以用它干任何事情!
这句话直接翻译过来是:不要只是检查错误,要优雅地处理错误。
所谓的「gracefully」(优雅地)其实只是「intentionally」(有意识地),也就是不理所当然地使用一种错误处理方式,不理所当然地觉得「发生错误了,我直接输出错误信息就好了」,而应该思考:我应该不应该增加更多上下文信息?应该不应该保存这个错误,做完其他事情之后再打印出来?
以下是写在 官方文档 里的,处理错误的例子:
for try := 0; try < 2; try++ {
file, err = os.Create(filename)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // Recover some space.
continue
}
return
}
for 循环的目的是尝试创建文件两次,如果第一次成功(err == nil),就 return 跳出循环,不执行第二次;如果第一次失败,那就对 err 进行类型断言(type assertion),判断这是不是一个 PathError,并进一步对错误进行检查,如果发现是空间不足导致的错误,就调用 deleteTempFiles() 函数删除临时文件恢复一些空间,然后 continue 进行下一次循环,再次尝试创建文件。
总而言之,不要把错误抛出来之后就真的抛之脑后了,想想自己能对这个错误做些什么!
这句话并不是 Rob Pike 在前文提及的演讲中发表的,而是写在 Go 官方文档里的 一句话 ,由于它和前面两小节讨论的错误处理相关,我就插到了这里。
直接翻译过来是:不要恐慌(panic)。这里的 panic 指的是 Go 语言中用于错误处理的一个函数,表示程序遇到了意料之外且无法处理的错误,必须结束程序。这句话很直接地告诉你:不要使用 panic() 函数。
文档很明确地告诉 Go 程序员,不要把 panic() 用来处理常规错误,任何能被预料的、有办法处理的(诊断原因并打印错误也是一种处理),都应该使用 error 类型的值,并将这个值作为函数的额外返回值返回给调用者,然后调用者再检查 if err != nil 来处理错误。原因在于,panic() 会直接结束程序,只有在程序真的完全没有办法继续执行,发生了无法预料的致命错误时,才应该使用 panic()。
即使是这样,panic() 也可以被恢复,即使程序被 panic() 停止,defer 的函数也会执行(前文有提及),程序员可以在 defer 函数中使用 recover() 函数恢复错误。以下是官方文档中的例子。
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
简单来说,除非真的没办法处理,否则不要 panic()。
这句话直接翻译过来是:设计架构,命名组件,用文档展开细节。
比起编程语言的基础知识,这条更像是软件工程的经验之谈,Rob Pike 说这是在 Go 语言中设计大型软件系统的方式,不过这对任何语言来说似乎都大有裨益。这句话总结了系统设计的三步骤:
这句话直接翻译过来是:文档是给用户的。
A lot of time when people write documentation, they say “this is what the function does”, and they don’t think about “what the function is for”.
很多时候人们写文档,会写「这个函数会做这个事情」,而他们不会想「这个函数是拿来干嘛的」。
这条也更像是工程智慧,而非语言的基础知识。在我的理解力,「这个函数是拿来干嘛的」其实是提供了相关的上下文,即「为什么一开始要设计这个函数」「为什么用户会用到这个函数」。在上一小节的例子中,safelyDo() 函数封装了 do() 函数,用于恢复可能出现的 panic,如果只考虑「做什么」,文档就应该是这样:
safelyDo()调用do(),并恢复潜在的panic。
但用户可能会感到困惑:我为什么会需要恢复潜在的 panic?不恢复会有什么问题吗?如果这样写,就会清晰不少:
由于
do()可能会触发不可恢复的panic错误,为了保证程序能继续执行,safelyDo()会恢复错误并不中断程序。
总结下来,我认为 Go 语言最鲜明的几条观点是:
syscall、Cgo 或 unsafe,减少使用不清晰的反射。我认为这是反技术债务且长期主义的,这避免了给未来的自己和开发团队留下隐患、增加工作量。interface{} 接收任意类型很好用,但自己真的无法确定参数的类型吗?直接打印错误看起来很常规,但自己有没有办法恢复错误,或者打印错误时能不能提供更清晰的上下文信息以供排查问题?我要实现的需求真的只能用危险的 unsafe 或 Cgo 实现吗,如果一定要脱离 Go 的安全区,我愿不愿意接受代码随时可能崩溃的代价?代码复用和不重复造轮子真的是任何时候都必须遵守的铁律吗,这个涉及第三方库的需求我真的不能自己写吗?unsafe 就失去了 Go 的保障。目前看来,Go 就是一门既安全又灵活,写代码既优雅又清晰,没有痛苦且观点鲜明的编程语言。Go 能用更少的代码写出更健壮的程序,很难令人不爱。
2025-11-24 10:55:00

Le Rouge et Le Noir
Oh, quel ennui, quel ennui ces gens bien gentils
啊,好无聊,好无聊,这些优雅的人儿Qui m’aiment et me lassent,
他们爱我也让我厌倦Sans rien qui dépasse
到无以复加Quel ennui, quel ennui tous ces beaux partis
好无聊,好无聊,所有这些佳偶Sous les politesses,
礼貌之下Si peu de noblesse
毫不高贵
玛蒂尔德小姐是我在《 红与黑 》里最喜欢的角色,这周看了由这部小说改编的音乐剧《摇滚红与黑》,玛蒂尔德小姐登场时唱的就是这首《Quel ennui》,也是我这部音乐剧里最喜欢的一首歌。喜欢她的原因,大概是在她身上看到了连现代人都难以拥有的主体性吧。
📢
📜
2025 年 11 月 18 日 11:20 UTC,Cloudflare 网络开始出现严重故障,无法正常传输核心网络流量。尝试访问我们客户网站的互联网用户会看到一个错误页面,提示 Cloudflare 网络出现故障。

此期间 ChatGPT 和各大网络服务均无法正常访问,中文博客圈也有不少网站出现了问题,本博客自然也没能幸免。Cloudflare 一直向互联网提供免费的 CDN 和防护服务,很多个人网站都接入了 Cloudflare,国际大厂也使用他家的服务。这次故障可以说让互联网的半边山都倒了,不过国内互联网应该是没有受到影响,而且事故发生时是北京时间的晚上,估计很多人是一觉醒来之后才发现出事的,而那个时候问题已经修复了。

故障并非由黑客攻击引起,而是 Cloudflare 内部的一个问题,和一行 Rust 代码相关。这是 Cloudflare 公布的代码截图。

问题出在机器人管理模块,这个模块通过机器学习分析网络流量的特征,拦截网络爬虫。事发当天,Cloudflare 更改了特征数据库的权限,原先用户只能访问 default 数据库中的表,他们在编写 SQL 语句的时候没有指定表名进行查询;权限放开之后,这个 SQL 语句查询到的不止是 default 中的特征。当这几百条特征值被上面的代码 unwrap() 之后,达到了预先分配的内存上限,而这个错误没有得到处理,所以导致了线程崩溃。1
很难想象,掌管着半个互联网流量的 Cloudflare,代码健壮性居然这么差,一个小小的权限改动就能引发蝴蝶效应。
这次事故倒没有引发我对 Cloudflare 的不信任,毕竟我也是免费用他家的服务,而且就算是 6 个小时的停摆对我这样的小网站来说也算不了什么;只是提醒了我,如果真要保证可靠的服务,是不能盲目相信任何大厂的,要把鸡蛋放在不同的篮子里。
📜
作者观察到,和 Open AI 多少有着联系的那些人,普遍相信人类真的能造出 AGI(通用型人工智能),并且相信 AGI 要么会让人类欣欣向荣,要么会导致毁灭。作者认为这种幻想不切实际,而且对真正的技术发展造成了阻碍。
I think it’s remarkable that what was until recently sci-fi fantasy has become a mainstream view in Silicon Valley. 我觉得很神奇的是,直到最近还是科幻内容的东西,居然变成了硅谷的主流观点。
So the belief in AGI, plus the recent results from LLMs, necessitates scaling, and justifies building data centres that consume hundreds of litres of water a second, run on polluting gas generators because the grid can’t supply the power (and might use as much power as entire cities), driving up CO2 emissions from manufacture and operation of new hardware, and exploits and traumatises data workers to make sure ChatGPT doesn’t generate outputs like child sexual abuse material and hate speech or encourage users to self-harm. (The thirst for data is so great that they stopped curating training data and instead consume the internet, warts and all, and manage the model output using RLHF.) 对 AGI 的信仰,以及最近 LLM 得到的进展,让规模化变得必要,让建造数据中心变得合理——每秒消耗几百升水;因为电网没办法供应能源,所以使用产生污染的燃气发电机(用的能源可能抵得上整个整个的城市);在生产和运作硬件的过程中排放大量二氧化碳;并且剥削、创伤数据工作者,来保证 ChatGPT 不会生成儿童性虐待内容、仇恨言论或鼓励用户自残自杀。(对数据的渴望如此强大,他们已经不再策展数据,而是直接吞下整个互联网,包括其中的瑕疵,并通过 RLHF 管理模型输出)
As a technologist I want to solve problems effectively (by bringing about the desired, correct result), efficiently (with minimal waste) and without harm (to people or the environment). 作为一个技术人员,我想要解决问题的方式是有效的(产生想要的、正确的结果),有效率的(最小的浪费),并且是没有伤害的(对人或对环境)。
If we drop the AGI fantasy, we can evaluate LLMs and other generative models as solutions for specific problems, rather than all problems, with proper cost benefit analysis. 如果我们放弃对 AGI 的幻想,我们就可以将 LLM 和其他的生成式模型作为特定问题、而不是所有问题的解决方案进行评估,而且要用恰当的花费收益分析进行评估。
总而言之,大语言模型是一项具有革新意义的技术,但人们应该看清现实,别再继续把大语言模型当作一切问题的答案了。
周二的早上爬不起来,错过了一节课,还被一个从来不点名的老师点到了。当天刚好降温,本以为是气温的关系,结果打开 Gentler Streak 一看,发现深度睡眠时间只有 31 分钟——恢复性睡眠不足才是我起不来的罪魁祸首!我在过去一年的时间把深度睡眠时间从半个小时调到了一个半小时(成年人的正常水平)2,当天睡眠质量急速下滑属实让我觉得猝不及防。
继续回顾睡眠数据,我发现当天的睡眠心率最高达到了 120 bpm,有很明显的峰值,这显然不正常。更令我害怕的是,这样的峰值在最近一周出现了四次。我开始回忆是什么导致了我睡眠心率飚升:
我实在搞不懂哪里出了问题,于是书呆子属性大爆发,做了个电子表格还画了图。



一开始我发现,深度睡眠(也叫慢波睡眠,Slow Wave Sleep,即图表中的 SWS)和恢复性睡眠(Restorative Sleep)的时长变化一致,深睡时间几乎决定了我当天有没有从睡眠中得到足够的恢复;而我的 SWS 时长几乎和我的夜间最高心率呈现负相关。所以,我一开始的重心放在了「什么引起了我夜间心率异常?」这个问题上,我认为是睡前进食、摄入甜味剂、热量堆积等原因导致了我心率上升,继而导致深睡时间减少。
然而,星期天的时候我发现当天的深睡时间只有 48 分钟,而夜间心率很正常;再回顾几天前的数据,也有不少心率高,但深睡时长健康的样本。所以睡眠质量下降可能只是睡前进食或摄入甜味剂直接导致的,接下来的一周我会避免在睡前两小时内吃东西,并且持续收集样本,看看睡眠质量下降的原因究竟是什么。
ℹ️2025.11.24(周一)补充:今天早上起来发现昨晚凌晨一点才睡,睡前还吃了黄油年糕、薯片这样的糖油混合物,深度睡眠竟然也达到了一个半小时以上,夜间进食似乎不是主要原因。我觉得最大的问题可能是健身补剂中的甜味剂。
打算丰富自己的文娱生活,于是想着每周可以看一到两部音乐剧。在周二的时候看完了《摇滚红与黑》,音乐剧删掉了原著里于连在贝藏松神学院学习的情节,大概是时长和经费不够(剧中的马匹和椅子等道具都是拿板子糊的,很难想象剧组穷成什么样子了),但整体而言我很喜欢。今年年初本来看到了《摇滚红与黑》要到成都巡演的消息,时间就是 11 月底,但错过了买票时机,没能去到现场,还是有些可惜的。周四看了上世纪八九十年代的《星幻》(Starmania),同样是一部法语的摇滚音乐剧,歌还不错,但剧情没有很感兴趣,所以没看完,之后可能会继续看。
看了《同乐者》(Pluribus)第四集,这一集似乎没有太多值得讨论的点,等全部九集更新完之后再评价吧。
《地狱客栈》第二季在这周播完了,社区骂声一片。大家并不是对结局不满意,而是对全部八集的剧情都不满意,以至于《地狱客栈》相关的内容下几乎全是情绪输出,搞得我看动漫的心情也不太好。我其实很少掺和社区里的讨论,如果从第三方视角来看的话,撇开所有的情绪输出,我认为《地狱客栈》第二季引起众怒的原因在于:
第二季最后是一个非常正能量的大团圆结局,同样地,这个大团圆也非常牵强(尽管从逻辑上来讲是说得通的,但感性上确实令人难以接受,像是硬凑出来的)。不过,该说不说,前面的打戏还是很好看的。
最后,我还想反思一下,我在看这个动漫的时候花了不少时间阅读评论和观看各种谩骂主创团队的视频,让我自己的心情和状态都变得很差。尽管我的确不满意这部作品呈现出来的结果,但还不至于那么讨厌,而这些谩骂有不少都是对主创 Vivienne Medrano 本人的人身攻击,还有不少在我眼里看来还算正常的人物成长、剧情发展,被一些人钻牛角尖指着骂4——其实我知道,这些人只是在发泄自己喜欢的作品没有达到预期的情绪,怒其不争而已,我没必要陪他们一起陷入到这种情绪里。我可能需要多加练习,才能做好一个冷静的观察者。
之前有人指责我不会读空气,还跟我解释日本文化很讨厌 KY,不过,某个文化的价值取向并不能佐证这一指控的正当性,而且我逐渐意识到,他所说的「不会读空气」和人们常说的 KY 并不是同一个概念。
所谓的读空气,就是根据面部表情、氛围等观察别人没有说出口的意思。和「察言观色」的区别是,读空气读的一般是一个环境、场合下,群体展现出来的言和色。读空气就是在特定的场合下做正确的事,KY(kūki ga yomenai,“空気が読めない”)就是指做出了不合时宜的事情、发表了不合时宜的言论。
回到前文提到的那个人,他之所以指责我不会读空气,并非是我经常做不合时宜的事情,而是我曾经在和他相处时,把他当做一个可以自由交流观点的对象,所以经常抱着刨根问底的态度和他说话,但在他眼里,这种刨根问底有些咄咄逼人,经常把他逼得说不出话来(尽管我怀疑是他没有能力表达清楚自己的观点,或者就是纯粹想要逃避表达,或者根本没有自洽的价值体系,被深入提问之后就回答不出来了)。在这种时候,他经常转移话题,而这种话题的转移在我看来显得不自然,我讨厌对话没有完结就被突然切断的感觉,所以会把话题扳回来,而这让他感到了不舒服。
在只有两个人的场合下,这真的能算作「不会读空气」吗?如果算,那这个空气不应该是在场的所有人决定的吗?而在他眼里,场上的氛围是「我不想继续讨论了,所以我转移话题,对面这个人应该读得懂我的意思」;但在我眼里,我的感受是「我希望讨论能有一个明确的结束点,明确表达结束或得出一个结论都好,话题转移使得讨论没有结束,所以我要把话题转移回来」。两个人的不同想法,哪个才是所谓的「空气」?
显然,对他来说,他通过转移话题想要表达的「我不想继续讨论了」这个言外之意没有被我接收到,而他认为这才是场上的空气,这个空气可以由他一人决定,而我没有读出他的心思,所以我不会读空气。按照这个逻辑,他自己也不会读空气,因为他没有读到「我希望讨论有一个明确的结束点」这个空气。
在两个人的场合下,KY 的指责不是自己社交能力低下的外归因吗?因为自己出于某些原因(可能是怯懦,不敢表达真实想法;可能是懒惰,不想解释;也可能是别的原因),不愿意直接表达自己的需求,而是旁敲侧击,希望别人能够读懂自己的心思,而没能读懂并顺着自己意思来的就是 KY——你他妈觉得全世界都是你男朋友吗?
九月份的时候读了《维罗妮卡决定去死》这部小说,我不喜欢,但有一句话我印象很深刻:
不要总是去猜别人是怎么想的。如果别人有问题,就会自己提出来;如果他们没有提出来,那就是他们自己的问题了。
这几天,这个朋友跟我说话的时候一直乱读我的空气。他实际上只是分享了一下自己有什么计划,没向我提出任何东西,我也就没有给出太积极的回应。他说「我觉得你可能不太感兴趣吧」,我回复「你为什么会觉得我不感兴趣?我觉得 OK 啊」,然后他就这么看着我,最后什么也没说,不知道是在哪里的莫名其妙的空气里读到了「我不想继续说话了」的信息。几分钟之后我去干别的事情了,他在一边自言自语「那看来只能我一个人去了」——你自己想找搭子又不自己提出来,那确实只能自己一个人去了。
我花了好长的时间才把自己这个高敏感性格锻炼成「不去刻意读言外之意;只听别人说出口的话;不多想不内耗;不预设恶意也不预设善意」的体质,在一些人眼里居然是不会读空气,这就是忠于自己的代价吧。
我想提醒那些喜欢不厌其烦地依靠言外之意沟通的人,在指责那些喜欢有话直说、不内耗的人没能读懂自己心思(或者称之为「不会读空气」)之前,想一想自己是不是真的把自己表达清楚了,对方真的应该去猜你的言外之意吗?所谓的「我觉得正常人应该都能懂吧」,有没有可能只是因为自己预设了所有人都和自己的思维方式一样,继而不愿意去接受别人的不同而导致的迷思?
对于其他的读者,你觉得「极度敏感;喜欢猜测言外之意而不关注别人说出口的话;预设善意或恶意并设下防备」和「善用直觉但在与人交往时不依靠直觉;关注并努力理解别人说出口的话而较少地考虑言外之意;只预设双方的平等而不预设善恶」,哪个是更健康的交流模式?