MoreRSS

site iconEltrac | 極客死亡計劃修改

常讨论心理学和社会观察相关内容,不敢说很懂哲学;也会写笔记和生产力工具相关内容;还是学生。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Eltrac | 極客死亡計劃的 RSS 预览

大脑充血 Vol.93

2026-08-10 08:28:27

本期周刊我将讨论《奥德赛》的三个版本:荷马史诗原著,Jorge Rivera-Herrans 改编的名为《EPIC》的音乐剧,以及由 Christopher Nolan 改编和导演的《奥德赛》电影。荷马的《奥德赛》我其实只读了第一章,诺兰的电影我也只是有所耳闻,只有《EPIC》从头到尾看过、听过好几遍,所以可能有失偏颇。

总之,可以把这期周刊当作《奥德赛》特辑,不过还辅以一些 LLM、旧新闻和牢骚。希望读者周一愉快吧。


吾仅悉落

EPIC: The Musical music cover

EPIC: The Musical

Jorge Rivera-Herrans

诺兰最近发行了新电影《奥德赛》,笔者身在中国大陆,还要一段时间才能看到,所以最近回顾了音乐剧《EPIC: The Musical》。这是一部同样由荷马史诗《奥德赛》改编的作品,音乐由 Jorge Rivera-Herrans 一人写作,音乐剧中的奥德修斯也由他演唱。《EPIC》的其他卡司1、动画和制作几乎全是由 TikTok、YouTube 等网络平台上的创作者合力完成的。可以说这是完全由业余爱好者制作的动画音乐剧,可音乐质量、叙事复杂度和情感深度都不输专业制作人,前不久貌似还公布了动画电影投入制作的消息。

我第一次听闻这部音乐剧是上大学之前,在 Bilibili 上看到了《 Done for 》的片段(这应该是 Demo,和正式发布的歌曲有出入),这段情节是奥德赛与女巫喀耳刻对峙的场面。当时的我仅仅是对音乐印象深刻,也没有意识到这是部完整的音乐剧制作计划。2022 年年底,第一个篇章《The Troy Saga》发布,直到 2024 年年底歌曲才全部放出。在 YouTube 和其他视频平台上可以找到由不同创作者绘制的动画拼凑而成的完整电影,时长为两个半小时。

音乐剧一共有四十首歌,分成了两幕和九个篇章。

  1. Act 1
    1. The Troy Saga:奥德修斯赢得了持续十年的特洛伊战争,不得不杀掉仍在襁褓中的特洛伊王子以免他在将来复仇,之后和六百名战士踏上回到家园伊塔刻的旅程。
    2. The Cyclops Saga:奥德修斯在岛屿上寻找食物,因杀死了独眼巨人波利菲莫斯的羊,激怒了他,随后失去挚友和船员。他最后戳瞎巨人,窃羊而逃。离开之前,他留下了自己的大名。
    3. The Ocean Saga:奥德修斯请求风神埃奧洛斯帮助,风神给予他装满风暴的风囊,告诉他只要不打开袋子就能回家,临近家园时因欧律洛科斯打开了袋子又被风暴吹回巨人岛,从独眼巨人的父亲波塞冬惊险逃脱。
    4. The Circe Saga:女巫喀耳刻将船员变成猪,奥德修斯在赫尔墨斯的帮助下战胜了女巫,喀耳刻随后帮助奥德修斯去到冥界找先知求助。
    5. The Underworld Saga:奥德修斯航行于冥界,遇见了他死去的战友和迟迟等不到他回家的母亲,受先知特伊西亚斯的话影响,他决定收起仁慈和软肋,不惜一切代价回家。
  2. Act 2
    1. The Thunder Saga:奥德修斯和船员用蜡堵住双耳抵御塞任的歌声并杀死了她们,随后穿过斯库拉的巢穴。麻木的奥德修斯任凭船员死去,激怒了欧律洛科斯,带领剩下的船员背叛了他。船员找到了最近的岛屿,杀牛充饥。奥德修斯百般劝阻船员不要伤害太阳神的牛群却没能成功,最后宙斯现身,让奥德修斯选择保留自己的性命,或者保留船员的性命。奥德修斯选择了自己活下去。
    2. The Wisdom Saga:奥德修斯的儿子忒勒玛科斯长大成人,而伊塔刻有许多人对王位图谋不轨。与此同时,奥德修斯被冲到女神卡吕普索的岛上,女神爱上了他并将他困在岛上七年。雅典娜寻求宙斯的帮助,最终赫尔墨斯前去要求卡吕普索释放奥德修斯。
    3. The Vengeance Saga:奥德修斯离开卡吕普索,用徒手制成的筏子回家,却发现海神波塞冬在家门口静候已久。赫尔墨斯将风囊又给予奥德修斯,奥德修斯打开袋子战胜了波塞冬,却掀起风暴阻拦了回家的路。奥德修斯用波塞冬自己的三叉戟反复刺向它,逼迫波塞冬停下风暴。
    4. The Ithaca Saga:奥德修斯回到伊塔刻,却听见图谋不轨之人密谋要杀死他的儿子、强奸他的妻子,奥德修斯与忒勒玛科斯相认,一起杀死了反叛者。最后,奥德修斯与妻子佩涅洛佩重聚。

如果对这部作品感兴趣,也可以到 官网 上购买 CD、彩胶和周边。我最喜欢的几个片段是由 Neal 绘制的《 Thunder Bringer 》、gigi 绘制的《 God Games 》、《 Keep Your Friends Close 》和《 Suffering 》,此外还有《 Wouldn’t You Like 》和《 Dangerous 》(谁会不喜欢赫尔墨斯……)。


心重如毛

仲树采访诺兰

📻

仲树观看了诺兰改编的《奥德赛》电影之后,在七月三十日采访了诺兰。这期采访之所以没有出现在上一期周刊,是因为上周的我没有听懂采访的内容。于是我这周补了不少《奥德赛》的情节,对诺兰的改编选择也有了些了解(尽管我还没有看过这部电影),这才可以继续。

采访中我印象最深刻的是以下几个主题:

  1. 诺兰是否基督教化了《奥德赛》?
    • 仲树说赎罪(atonement)这个概念并不存在与古希腊文化中,更没有出现在荷马的《奥德赛》中。诺兰的奥德修斯却是以忏悔罪人的形象出现的,这更接近基督教文化。
    • 诺兰说他在改编时并没有刻意选择某一条哲学路径,也就是说他并不是想要基督教式地改编《奥德赛》。赎罪的概念可能没有直接出现在原著中,但他认为古希腊文化中的 Xenia(好客之道,或者说「用你想被别人对待的方式对待别人」)与如今的价值观有共通之处。他关注的不是哲学理论的区分,而是理念的共通性。
  2. 识别出叙事的套路(或者说模式)是否就让套路失效了?
    • 诺兰在采访中表达了他对业余影评人的看法,业余影评人经常在识别出电影的套路、搞清楚为什么电影会以某种方式影响自己之后,就把这种套路「无效化」了——用更本土的说法来讲,我想可以称作「祛魅」。业余影评人热衷于对电影叙事的模式祛魅,然而仅仅是观察到模式并不代表模式就变得低劣了。电影无法脱离套路凭空出现,因为电影是一种人类共通的叙事语言,抛弃套路就是抛弃语法句式。熟悉的模式当然会让人觉得无聊,诺兰表示这的确是电影导演要考虑到的,电影应该主要建立在人们熟悉的模式之上,此后才能引入新的模式。这也是他对传统和创新的理解。
  3. 电影中的神明究竟是什么?
    • 这一条是我自己的观点:当我得知诺兰的《奥德赛》里没有宙斯、赫尔墨斯、波塞冬、埃奧洛斯和莲花食者的时候,我感到震惊,这难道不是把荷马史诗里的整个希腊神话世界观都剔除了吗?甚者,为数不多的保留下来的神明几乎都失去了「神性」,雅典娜更像是奥德修斯的投射和想象,而不是相互嘲弄的朋友,爱上奥德修斯的卡吕普索明明困住了他七年,在电影里却表现得像个心理医生。老实说,目前看来我很失望,但最终的评价还是要等到去看完电影之后才能作出。
    • 采访中诺兰表示神明对故事中的角色来说是真的,当时的人们就像我们接受科学概念一样,理所应当地接受了神明的存在,比如雷电就是宙斯在说话。神明更多是电影中角色的投射。这么说来,诺兰的电影里的确没有真的神明,只有「会被古人误以为是神明的自然现象」以及他们文化中对神明的信仰。不得不说,这是相当现代化的改编,我还不能确定我喜不喜欢。

其实音乐剧《EPIC》也对荷马的《奥德赛》做了不少改编,比方说原著中的奥德赛其实数次出轨,他与女巫喀耳刻同床了一年,被卡吕普索困在奥吉吉亚岛上的七年,他也与女神共枕,而《EPIC》中的《奥德赛》则十分忠贞和坚定,诺兰的《奥德赛》则完全没有提及性。我想这是因为《奥德赛》中的一些故事情节可能令现代人难以接受,或者说改编者选择的切入角度不同,有些东西被忽略了。

Jorge 或许出于艺术表达的偏好或呈现效果改编了不少情节,比方说奥德修斯杀死襁褓中的特洛伊王子是受到宙斯的提醒(甚至威逼利诱),而原著中这个情节却没有出现宙斯,雅典娜请求宙斯释放奥德赛也没有像《God Games》里那样受到重重阻碍,原著中的宙斯其实相当欣赏奥德修斯。诺兰也因为对电影主题的关注点不同而做出了相当大程度的修改,因为无论是原著还是《EPIC》,奥德修斯从来没有为自己打赢了特洛伊战争而羞愧过,而赎罪却是诺兰电影的一大主题。

我多少有些为荷马不平,层层修改之后人们真的还在乎这位伟大诗人想要表达的东西吗?可后续的创作者也有自己想要表达的东西,难道他们的想法就不该得到实现吗?我不知道怎么回答,我只能说这三个版本的奥德赛都是很不同的作品。

诺兰如何基督教化了《奥德赛》?

📻

有不少其实都在上一节里写过了,这一节主要谈一点:追求光荣如何变成了骄傲和狂妄?

荷马作为古希腊诗人,实际上是追求光荣的。这有点像古典哲学中的美德的概念,古希腊人和古罗马人认为男人应该追求美德,Virtue 的本意是「像男人一样」,更接近所谓的「阳刚之气」。古人的思想无疑带有一些厌女的气息(读《奥德赛》第一章就能读出来,忒勒玛科斯和佩涅洛佩说话的方式缺少对母亲的尊重,更多是「身为家里唯一的男人」的教导语气),一定要战胜敌人、证明自己的想法或许也有些野蛮,但把古典式的「战争的光荣」完全现代化为「需要赎罪的愧疚」无疑让整部作品都走向了另一个主题。

仲树和诺兰的演讲也提到了类似的主题,现代人似乎要求伟大的人为自己的伟大鞠躬道歉,否则就是傲慢。可是骄傲(Pride)难道不能被合适地给予那些值得骄傲的人吗?我们难道不能允许真正卓越的人带着一些傲慢吗?一个母亲难道不应该被允许要求长辈和旁人不要干预自己的教育之道,允许她带着些许的傲慢养育自己的孩子吗?

奥德修斯的狂傲(hubris)体现在他对神明的挑衅,最终使得手下的六百人全部丧命,只有他一人活了下来(例如,在与独眼巨人的周旋中,奥德修斯本没有透露自己的姓名,他自称「没有人」骗过了巨人,却在离开时想要巨人记住是谁戳瞎了他,喊出了自己的名字,这直接导致最后波塞冬杀死了他的船员)。奥德修斯最终还是回到了家里,与妻子佩涅洛佩和儿子忒勒玛科斯重聚。在诺兰的改编中,他为自己的傲慢付出了代价,是在赎罪(atone)之后才得以与妻子重聚;在《EPIC》音乐剧的改编中,奥德修斯接受了「自己成为了一个怪物」的事实,不惜一切代价与家人相聚——电影的主题是人不能打破 Xenia,要为自己的行为赎罪;音乐剧的主题是一个开放性的问题:什么时候一个人会变成怪物?

奥德修斯在一路上的确失去了太多,他没有理由保持理智。《EPIC》中奥德修斯不断失去的东西成为了压垮他的一根根稻草,成为了他自我蜕变的契机,甚至是一种修炼。诺兰则让一次次的失去成为奥德修斯的警钟,奥德修斯将这些灾难解读为打破了 Xenia 的后果,他需要赎罪。两种解读都很耐人寻味。

如何不把人当成猪?

📻
📻

《奥德赛》中的女巫喀耳刻把奥德赛的船员变成了猪,在音乐剧《EPIC》的改编中,喀耳刻这么做是为了保护她和她的神女(nymph)。把陌生人都当作会为自己带来灾难的威胁或许是谨慎,而在没有生存威胁的情况下不关心具体的人,把具体的个人抽象为标签式的「猪」,只能是愚蠢。

仲树与诺兰的采访在外网的反响其实相当好,这为她引来了很多关注和媒体报道。由于国外的大多数人并不认识她,所以许多人将她称作 The China Lady。对这场演讲的反应大概可以归为几类:

  • 为什么中国人对《奥德赛》的理解比美国人深刻?为什么美国人自己的采访者只会问一些蠢问题?
  • 不,仲树是在美国的教育体系里培养出来的,她代表的是美国人。
  • 不不,仲树活跃在中国的媒体环境里,中国能给她表达这些内容的环境,而美国显然没有。
  • 仲树是中国政治宣传的工具,这些问题是中国政府安排给她的。
  • 仲树翻译了阿伦特的书,而阿伦特是犹太人,所以仲树实际上是受到犹太人集团控制的。
  • 仲树的英语口音很重,讲话磕磕巴巴的。
  • 他们怎么选了长得最辱华的一个中国人去采访诺兰?
  • 她怎么穿个拖鞋就去采访了?

咳,后面三个都是中国人说的。我不谈这些具体的话语究竟为何出现,因为就像仲树自己说的,这很庸俗。它们的共同点是,他们完全没有在意仲树和诺兰这两个具体的人究竟说了什么,为什么会这么说,有什么意义,从中能获得什么。他们把仲树抽象成了「中国人对《奥德赛》的理解」「中国媒体环境的代表」「美国教育的成果」,把诺兰抽象成了「被政治正确污染的激进派」(因为包括雅典娜在内的一些角色是由黑人演员饰演的)「完全不懂古典哲学的麻瓜」等等。

公众和媒体似乎在猎巫,但究竟谁是女巫?被变成猪的人,还是那些把他们变成猪的猎巫者?


鸡头蛇尾

Last.fm 独立

📜

我在《 去营地整点 FLAC 》里提到 Last.fmParamount 旗下的服务。就在今年五月,它独立了,目前正作为独立公司运营。

Last.fm 表示用户数据、功能、API 以及背后的开发团队都没有改变,仅仅是公司的所有权转移了。

What does independence mean for users?

It means we can focus fully on building listening insights and community features for music fans. You’ll see continued, steady improvements over time.

如果你不知道 Last.fm 是什么:他们提供 Scrobble API,你可以用任何音乐播放器将播放记录发送到 Last.fm 账号,它会统计你的播放记录,生成周度、月度和年度报告,还会推荐你可能喜欢的音乐和音乐人给你。如果你使用本地播放器听音乐,或者有很多歌曲散布在不同的流媒体平台,Last.fm 可能很有用。

看到自己依赖的软件服务不再依赖大公司真是欣慰,至少我更放心把数据交给他们了。

软件工程与生成式人工智能的迷思

📜

文章列举并反驳了生成式人工智能在软件工程中应用所产生了八个迷思:

  1. 开发者的大部分时间都用来写代码。实际上有研究表明开发者只有 14% 的时间在写代码,其他时候基本都在开会、讨论方案和设计。
  2. 写代码是瓶颈。如上所述,写代码只占开发者 14% 的时间,加速写代码不会让开发速度显著提升。
  3. “AI” 生成的代码行数足以证明其影响。比尔·盖茨说:用代码行数衡量生产力,就像是用重量衡量一架飞机。
  4. “AI” 平等地帮助所有任务、所有工程师。研究发现用 “AI” 做已经被理解、很熟悉的事情,提升的效率才会更高;此外从业经验与编写高效提示词的自信心呈负相关,研究发现 “AI” 延长了那些有经验的开源开发者实现代码的时间——我猜这是因为缺乏经验的人更容易忽略 LLM 生成代码中的模糊点,也不那么吹毛求疵。
  5. “AI” 能让一个开发者干十个的活儿。如前文所述,真实的软件开发还涉及协作,这才是最大的瓶颈。效率提升与任务的种类也有关。
  6. 让 “AI” 变得好用是开发者自己的事。“AI” 工具不是天然地好用,而且缺乏必要的指导。“AI” 可能是为数不多被企业花大价钱引进,却不知道如何最大化其价值的工具——这个我也有同感,LLM Agent 的使用方式模糊多变,企业不得不引入额度限制或额外教导使用方式,才能保证员工不因为「不会上下文管理」而烧掉太多的钱。事实不是开发者不会用,而是它本身就不好用。
  7. 表现很好的 “AI” 工具会自动被采用。“AI” 不总是很容易融入现有的工作流,需要结构化的调整。
  8. 用了生成式 “AI”,大企业就可以像初创企业一样快速地发行软件。人们对初创公司的期待不同,能够容忍更多的缺陷,而人们对大企业的可靠性有更严苛的要求。

作者的论述很完整了,所以请允许我插一句与原文无关的话:我发现几乎所有谈论 LLM 的人,无论持什么态度,几乎都避开了版权议题,除了 Codeberg 。事实就是,LLM 生成的代码是从开源软件的源代码扒下来、拼凑起来的,而开源软件有授权协议,使用这些代码需要遵循协议。有的协议要求使用者自己也开放源代码,有的协议非常宽松,但也要求署名。无论原本的代码是什么协议,从 LLM 输出之后便不可知了,开源协议被洗掉了,从此任何人都可以没有代价地使用他们本应该付出代价才能使用的代码。这难道不可怕吗?为什么其他人都避开这个话题?为什么我数次在博客提出这个问题都没有人回应?是因为它太难解决了吗?

大型组织会花费大量时间在讨论无关紧要的琐事,但是真正重大的决议反而可以轻松过关这种现象。这是由于人对大议题较难有全面性的理解,故怕贸然提出异议,可能会失言;相反地,对于一些简单琐碎的小事,有相当的认识,因此意见特别多,造成组织在各事项上讨论所花费之时间,通常与事项本身的重要程度呈现反比。

—— 帕金森琐碎定理 - 维基百科

以前还有画师会公开反对自己的作品被用于训练模型,如今他们的声音似乎也消失了。是成为科技进步的牺牲品了吗?真是令人唏嘘。我对企业不抱任何「突然良心发现」的期待,但开源社区至少应该开始讨论它。

烤牛排不需要技巧

📜

烤牛排不需要技巧,开火,淋上油,把牛排放上锅煎,一段时间后翻面就好。谁都能烤牛排,但不是所有人都能烤出好吃的牛排,甚至仅仅是烤出还行的牛排都很难。开发软件也逐渐变成烤牛排一样的活动,所有人都能写软件,但不是所有人都能把软件做好。

很有意思的比喻,我大概也会用这个比喻去说服持有不同观点的其他人。不过,我并不完全赞同。和上一节所述的观点一致,烤牛排仅仅是制作菜肴、宴请宾客的一小部分活动而已。烤牛排并不涉及到「所有人都想要一块好牛排,于是一起讨论怎么烤牛排」的活动。

LLM 或许解决了怎么开火、用什么工具给牛排翻面更容易、怎么防止牛排粘锅的问题,但正如 Frederick Brooks 在《 No Silver Bullet 》里写的,这些仅仅是偶然性问题。对烤牛排而言,最难解决的是自发性问题(或者说内源性问题),怎么判断一块牛排到几分熟了、怎么知道多久翻面一次、怎么根据牛排的种类和大小调整烹饪方式等等。


漫步遐思

呃,我好像已经连续两周没有写这一节内容了,牢骚都在联邦宇宙上发完了,这之后的想法要么写成文章了,要么就根本没有出现。下班之后我就在读书,瘫在书桌前看剧,或者一边听《EPIC》一边乱逛。最近在看音乐剧《Little Shop of Horror》,或许你也会感兴趣?

不过说起来,我的精力的确变低了。读书变慢了,用来读书的时间也变少了。这其实不是因为上班太累太烦导致的,工作其实还算轻松,同事人也都很好。只是,我可能不适合在公司的环境下和人产生那么多的接触吧。尽管没有那么多接触,但我还是会感到社交压力,回到家就没什么力气了。每天只有早晨最有活力,可惜我这周有好几天睡太久了。或许会习惯这种生活的,会吧?


  1. Cast(演职人员)的意思,一般戏剧、音乐剧的演员都被称作卡司。 ↩︎

新知识分子的新庸俗

2026-08-09 15:27:49

首先定义新知识分子,不是什么专有名词,只是我在找到更合适的称呼之前暂用的词。我所指的新知识分子,是经过现代教育系统培养的,相对突出的一部分人。虽然他们没有成为学者或真正顶尖的精英(当然这部分人也有他们的庸俗,只是本文不谈),但他们仍会获取知识并实践。他们对待知识没有学者的求是态度,他们的态度更加功利,只要知识听起来没有特别离经叛道,或者能够被一套话语体系说服,他们就乐意成为实践者,并将那些他们自己也说不清楚是什么的东西当作真理。

如果这个时候我开始罗列群体的名字,想必不太明智。等我先讨论完问题本身,再考虑要不要引火上身,更何况,我也或多或少属于此类群体,共享着他们的庸俗。

什么是庸俗?英文里的 Vulgarity 带有一些粗俗和野蛮的意味,为什么知识分子也可能是野蛮的?另一种定义是说庸俗者缺乏品味和教养,没有这两种东西的人如何成为知识分子?掌握知识并不会让知识分子失去 人性 ,而人性本身就包含动物性和智性。新知识分子可能在某些方面开发了自己的智性,但另一些方面仍未开化,动物性也并非退居后位。以一句网络名言为例,「不学数理化,生活处处是魔法」,说这些话的人也仅仅是受过九年义务教育和高中教育而已,或许还有本科教育——正是因为他们对智性的开发不完全,所以不能理解不是所有人都与他们受过同等教育的事实,他们更不会关注背后的历史、阶级和资源分配问题,拒绝承认世界上有除科学以外的看待世界的方式;也正是因为他们的动物性仍然猖狂,才会以试图以自己并不博学的头脑嘲笑看似比他们更粗俗的人,就像自己肌肉并不发达的人去欺负乳臭未干的小孩子一样。

不过本文要关注的,更多是新知识分子在智性层面显现出来的庸俗,也就是问:新知识分子为什么缺乏品味和教养?他们为什么庸俗且媚俗?他们眼里只有一两种可以接受的生活方式和文化符号。他们刻奇,自我感动。他们拒绝看见与他们不同,尤其是比他们更弱小的人,他们否定人的多样性,把与他们不同的人都当作堕落者,把比他们更高的人供奉为神。他们其实和大众没有什么不同,新知识分子和大众都陷入到一种抽象观念或狂热的群体情感当中,蒙蔽了双眼,看不见具体的人和具体的事,对一切都漠不关心,却又想要发表意见,显得傲慢无礼。

在进一步阐述新知识分子与大众的共同点之前,我想先用一个例子将两者区分开来。如今的大众是不读书的,就算他们真的打开书本,也很少是被求知欲驱动——他们要么处在学校或某些无法行使自由的环境下被逼迫读书,要么是听信「知识改变命运」的陈词滥调 悲壮 地阅读,要么是机械地复述甚至误读书本以便显得见多识广,又或者是附庸风雅。简单来说,他们缺乏对真理的追求。新知识分子其实也没有这层追求,但他们中的大多数地的确有着对阅读的信仰,因为他们觉得阅读的行为是高尚的、神圣的、纯粹的。

我曾经在这样一个群体里待过,他们的本职工作其实是销售课程,但他们的内部会议和精神文化都相当「积极向上」,他们会把成功完成一单交易称作「帮助到一位学生」,他们也从来不用「销售」这个字眼谈论自己的工作,以至于外人(包括从未真正融入的我)觉得他们像传销组织。他们时常在会议中分享成功学观念,他们也阅读,而且是实打实地读书,也会认真地准备读书分享。他们常常复述一些理论(有些仅仅是价值判断而非理论),比如「要有输入有输出」「时间管理很重要」「要照顾好自己」「要规律运动」等等。我还见过一个主要为职场中的年轻人生产访谈内容的博主,她偷拍了站在书店角落里读书的人并发在动态里,表示「总觉得这样的人有种神性1」,可见他们对阅读这一普通人类行为的过度神圣化——这种神圣很大程度上来自于公众对阅读的放弃,而非阅读本身有多神圣。其实这些描述就已经是我要谈的新知识分子的一个缩影了,他们在某些方面不同于大众,但又不能被归类为精英。

这类行为已经有一个名字:媚俗(Kitsch),或者说刻奇。媚俗是对已经被广泛接受和认同的艺术的毫无价值的模仿,比方说有人因为《蒙娜丽莎》备受赞誉,试图在自己的作品中也刻画一种神秘的微笑,这便是媚俗。媚俗不仅存在于艺术中,也可以是对情感的重演和复刻。一位老人看见充满年轻活力的少年在玩耍,心中感慨万分,回想起自己年轻时的样子,对生命生起一丝敬畏,这是自然的情感反应。可如果老人开始审视自己的感动,也就是说他意向的对象从具体的少年和真实的情感反应变成了抽象概念,他觉得自己感受到生命之美和青春活力并为之感动的意识活动十分圣洁,他便向媚俗靠近了。如果另一位老人听了这位老人的描述,也开始赞叹「祖国的花朵!」「民族的希望!」「八九点钟的太阳!」,并为此流泪,这便是彻彻底底的媚俗,他并没有真实地经历过前一位老人体会过的情感,更没有真的关心少年。我逐渐意识到 我对感动过敏 的原因是我受不了媚俗。

新知识分子总是试图在自己的生活中复现那些圣洁的东西,在商业活动中歌颂自己的伟大,一边听取精英的观念一边沐浴在自己的「成长进步」当中,拿着相机侵犯隐私以陶醉于自己发现神性的眼睛。

他们沉醉于阅读的神圣性时,并不关注真理和文本的深度,所以他们阅读的常常是畅销书,尤其是那些自助类的畅销书。如果仅仅是「如何进行资产配置」「如何获得健美的肌肉」一类的书,那还不足为奇。那些细碎的特征我就不列举了,我只需要给你举一个最具代表性的例子——《认知觉醒》。这本书的目录就散发着媚俗的气息,以下是其中的一些标题。

  • 人生是一场消除模糊的比赛
  • 顶级的成长竟然是“凭感觉”
  • 成长慢,是因为你不会“飞”
  • 我们生而为人就是为了成为思维舵手
  • 深度沉浸是进化双刃剑的安全剑柄
  • 深度学习,人生为数不多的好出路
  • 你没成功,可能是因为太刻苦了
  • 这个世界会奖励那些不计得失的“傻瓜”
  • 冥想:终有一天,你要解锁这条隐藏赛道
  • 运动:灵魂想要走得远,身体必须在路上

最具典型性的是第八章的章节名:「早冥读写跑,人生五件套——成本最低的成长之道」。「早冥读写跑」很像是纽约时报畅销书和英文世界很喜欢造的方法论缩写,缩写看起来很像黑话,增加了理解门槛,而门槛的增加则塑造了神秘感和学习成本,使得通俗理论看起来像是需要努力发觉才能掌握的真理。

值得玩味的是,书中其实并没有什么错误观念,没有做了就能成功的虚假承诺,和典型的成功学有很大区别。新知识分子可能自己都排斥成功学,把成功学当作像是没学数理化的人会以为是魔法的东西来嘲弄。

事实是,自助类畅销书的作者往往不是什么专家,他们只是比常人更自信,也的确有一些知识积累。他们写在书中的想法和理论有一些价值,但往往是根据自己有限的经验和认知得出的,而这些有限的经验和认知,有不少仅仅是缺乏理解的复述。如果你读过《认知觉醒》,你可能会觉得这是一本看起来连贯但实际上是由各种其他人的观念拼凑起来的书。这些观念原本是谁呢?精英的。

我在前面提到过,新知识分子眼里只有一两种可以接受的生活方式和文化符号,「早冥读写跑」何尝不是其中一种?人们在谈论早起、冥想和阅读等生活方式的时候,往往拿 Elon Musk 一天读一本书的故事和硅谷成功人士每天四五点起床、每天打坐一小时的故事作为宣传材料(当然,也会辅以一些早睡早起身体好、冥想有助于提升专注力和学习力等实际上不一定有充足的文献支撑的证据,复述别人的引经据典也是新知识分子感到高尚的行为)。新知识分子其实在模仿精英的生活方式。

模仿也是媚俗。精英可能真的阅读过文献并切身体会过这种生活方式的具体好处,并且是在多次迭代和转变之后形成了如今的生活方式,他们也对未来可能会发生的改变保持开放。新知识分子并没有具体地亲身体会或赞同某种生活方式,他们只是媚俗地模仿,并复述精英们说的话。

以上或许只是对品味的批评,庸俗难道不是缺乏品味和教养吗?缺乏教养怎么体现?新知识分子或许不会像,比如说某些右翼的美国大众那样,因为自己的信仰就剥夺所有人堕胎和与同性结婚的权利,但新知识分子的确有类似的思想倾向,区别仅仅是他们懂得一些礼貌。请想象,你在分享很具体的生活细节,可能是最近发生在生活中令你感到焦虑的事情,而某个人突然开始向你分享正念冥想有多么好,复述名人名事和「冥想能缓解焦虑,提升专注力」等陈词滥调。你可能只是想要事后再做深入了解,也可能是你尝试过,但冥想并不能解决实际问题,总之,你没有在当下立刻成为他的「同类」。他将你认定为不思进取,但嘴里还说着「我觉得有些人走得快点,有些人走得慢点吧」「没关系,你不用和我一样」的鬼话。他没有直接发起任何攻击,但你能从他的眼睛里看到,他在审判你。

他自以为是正义女神,唯一的区别是他没有被蒙住眼睛,他被自己想象的圣光刺瞎了。他没有看见你真实的烦恼,不关心具体的人所经历的具体的事件,他只关注或许是公正、或许是光荣、或许是伟大的抽象概念,沐浴圣洁的光辉当中,已经看不见真实世界。

我不觉得这是精英主义,因为精英的庸俗、傲慢和不食肉糜体现在另一层面。精英主义更多地是阶级议题,新知识分子还没有触及到政治层面,而且更加常见。


  1. 人对神性的谄媚和渴求,我已在《 何以为人 》中批评过。 ↩︎

艾尔特拉克在岣琅

2026-08-05 23:11:35

汉娜·阿伦特报道了阿道夫·艾希曼在耶路撒冷因组织屠杀犹太人受审的案件,在法庭上,她发现艾希曼并不是一个冷静且思维缜密的杀手,相反,“说话时间越长,越能感受到他表达力的匮乏与思考能力的缺失,他不会站在别人的立场思考问题,之所以无法同他交流,并不是因为他说谎,而是因为他周围围绕着坚固的壁垒,屏蔽他的言辞和他人的存在,从而帮他一并拒绝着真相。”她震惊于恶的平庸性(Banality of Evil)。

艾尔特拉克和蓝色地鼠一起编写健壮的软件,在编辑器面前,他发现 if err != nil 并不是什么精巧的错误处理机制,相反,编程时间越长,越能感受到地鼠错误处理机制的缺失,他不会帮程序员抛出和拦截错误,之所以无法用条件判断以外的方式处理错误,并不是因为地鼠不想,而是因为已有的简单工具就足够提升程序的健壮性。他震惊于错误的平庸性——错误仅仅是包含 Error()接口 而已。

魂断杰威幕

众所周知,嘲讽 Java 是这个博客的传统,所以今天的文章也以这个开始。在 Java 中,处理「错误」的方式是抛出异常。这里的异常(Exception)是指程序中可以预料到的问题,比如用户本应输入整数却输入了非数字字符,而错误(Error)在 Java 中指的是那些几乎无法恢复,会使程序退出的问题。

是的,术语开始混乱起来了,详见下表。

Java Go
可预期到的、程序应处理的问题 异常(Exception) 错误(Error)
没有预料到的、难以恢复的问题 错误(Error) 崩溃(Panic1

为了方便讨论,在正式讨论 Go 语言之前,我会避免使用「错误」这个词,统一使用「异常」和「崩溃」两个词。老实说,「崩溃」这个词也不准确,Java 程序如果抛出 Error 不一定会终止程序,而 Go 程序如果 panic 也能够恢复,但姑且先这么用着吧。

Java 的异常和崩溃共用一套机制,他们甚至共用一个基类 Throwable(可抛出类)。Java 的异常和崩溃都是被抛出(throw)的,而抛出 Throwable 的函数的调用者可以接住(catch)被抛出的东西。不过 Oracle 的文档对 Error(崩溃)和 Exception(异常)两个子类的描述很有意思。

The class Exception and its subclasses are a form of Throwable that indicates conditions that a reasonable application might want to catch.

—— Exception - Java Platform SE 8

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.

—— Error - Java Platform SE 8

简单来说,异常应该被接住,而崩溃不应该被接住。任何 Throwable 只要没被接住,就会使程序崩溃,提示 Unhandled Exception(未被处理的异常)——等等,你有没有发现,没有被接住的明明是 Throwable,为什么提示信息会是 Unhandled Exception?难道 Error 不也是 Throwable 吗?

是的是的,我们已经发现 Java 让异常和崩溃共享同一个机制引发的问题了。异常和崩溃都可以被抛出,但区别在于崩溃不该被接住,而异常应该被处理。没有处理的异常会导致 Unhandled Exception,而本就不应该被处理的错误也会导致 Unhandled Exception。Java 还有一个 UncaughtExceptionHandler 接口,用来处理线程里那些没有被接住的异常,我们来看看接口中包含的方法签名。

interface UncaughtExceptionHandler {
 void uncaughtException(Thread t, Throwable e);
}

啊,是的,代码里写的是 Throwable,可名字里明明是 Exception 呢。显然 Throwable 作为父类的抽象层次比子类 Exception 高,而熟悉 OOP 的程序员应该知道,父类不应该知晓子类存在,而一个为父类设计的接口竟然包含子类的名字,尽管没有直接依赖子类,但显然不是好的设计。更何况,我们已经体验到这种做法造成的迷惑和混乱了。

如果崩溃不应该被接住,那它一开始就不应该与需要被接住的异常归属于同一基类。如果子类之间的行为不同,就违反了里氏替换原则。Always glad to find design flaws in Java.

我们一直在谈抛与接,是时候来看看代码里怎么写这两个东西了。「接」想必各位都很熟悉了,try ... catch 语句在 JavaScript 等语言中也能见到。

try {
 doSomething(); // 这个函数抛出异常
} catch(IOException e) {
 // 接住 IOException 
 // 处理这个异常
} catch(Exception e) {
 // Exception 是所有异常的基类
 // 处理剩下其他类型的异常
}

如果不把 doSomething() 包裹在 try 里会怎么样呢?你会得到 Unhandled Exception 信息。为什么会这样呢?因为 doSomething 的方法签名声明了异常。

void doSomething() throws Exception;

throws 关键词用在方法签名中,表示这个方法会抛出异常,而调用者应该抓住这个异常。如果声明了多种不同类型的异常,那么调用者应该抓住所有类型的异常,或者…… 只写一条 catch(Exception e)。如果调用了可能会抛出异常的方法却没有放在 try 里面,就会得到 Unhandled Exception

真的吗?如果我就是不想要写 try ... catch 呢?

那就让 Java 教会你踢皮球,把这个错误抛给你的调用者就好了!

// 这段代码会出错
doSomething();

// 这段代码处理了异常
try {
 doSomething();
} catch(Exception e) {
 // ...
}

// 这段代码向上抛出了异常
void faireQuelqueChose() throws Exception {
 doSomething();
}

老实说,我写 Java 程序的时候就这么干过好几次,大不了最后这样写:

public static void main(String[] args) throws Exception {
 // 想怎么抛怎么抛!
}

不过异常具体是怎么被抛出的?

void doSomething() throws Exception {
 // 有不对劲的事情发生了
 // 赶紧抛出异常告诉调用者
 throw Exception("Something weird happened!")
}

嗯?这是个新的关键词吗?看起来是的,用在方法签名上告知调用者可能抛出的异常类型的关键词是 throws,而用在方法体内部抛出异常的语句是 throw。这个语句和一个标记了 throws 的方法调用在某种程度上是等价的,在方法体里写 throw Exception() 和可能会抛出异常的 doSomething(),都需要做异常处理,要么放在 try ... catch 里,要么往上抛让调用者处理。

原来 Java 程序员在一开始就要区分 throwthrows 这种细微的区别,怪不得我那个半吊子 Python 老师这么喜欢 纠结语言保留字

与此同时,Lisp 程序员:语法…… 语法太多了…… 看不懂……

No Exception

让我们总结一下,在 Java 中,异常和崩溃都要被抛出。抛出异常就等于说是要调用者(或者调用者的调用者)接住并处理这个异常,把错误处理的责任向上传递。这其实很符合逻辑,因为有一些错误就是没办法在方法内部解决,比如一个发起 HTTP 请求的方法遇到请求超时就只能上报问题。抛出错误就等于说是要程序因无法处理的错误而终止。

抛与接看起来很精巧,许多人都喜欢此类机制。如果机制是必要的,那么机制的存在使得学习和理解的成本上升,使得语言变得臃肿,或者引发机制内部的混淆与问题(比如 Error 也算 Unhandled Exception),似乎也还算能接受。可问题是,机制真的是必要的吗?我们真的需要这一层额外的抽象吗?

程序中应该被解决的错误被称作异常,抽象为 Exception;程序中无法解决的错误被称作崩溃,抽象为 Error。这两个类都是被抛出的,被称作「可抛出类」,所以抽象为 Throwable。为什么需要最高的这层抽象?这种抽象真的合理吗?它的子类难道不是两个很不同的东西吗?

思来想去,让 ErrorException 归为一个基类的原因,我只能想到「它们都是被抛出的」这一个。正方形和矩形都是「可求面积的」,那么就可以都归类为 Squarable 吗?显然不能,正方形不能继承矩形已经是里氏替换原则的一个经典案例,让正方形和矩形都归为一个基类更是荒唐。类比之后可以发现,ErrorException 也是如此,仅仅因为想让它们共用抛出机制就归为一个基类是愚蠢的。仔细看的话,Error 倒像是 Exception 的一个不需要被接住的例外(exception),而 Oracle 自己对 Exception 的描述却是「conditions that a reasonable application might want to catch」,所以到底接还是不接呢?好像不接也行,那 ExceptionError 真的有区分的必要吗?但好像不区分也不行,毕竟确实是两个概念。

是时候终止这场混乱了。

“Errors are values.”

这句话是 Rob Pike 说的。其实我还想更进一步说:Errors are conditions. 这里的 condition,既是状况,也是条件。仔细想的话,如果没有错误处理机制,我们就是会用条件分支语句处理异常情况。

func addThreeNumbers(numbers []int) string {
 if len(numbers) > 3 {
 return "too many numbers"
 } else if len(numbers) < 3 {
 return "too few numbers"
 } else {
 return strconv.Itoa(numbers[0] + numbers[1] + numbers[2])
 }
}

假设这个错误在内部处理不了,需要交给调用者处理。比方说,某个函数仅仅是读取文件,它并不知道读取文件是为了什么,也就不会知道要是文件不存在应该怎么办。假设这个函数的调用者是为了读取配置文件,而它知道,没有文件的时候就写入默认配置。函数的调用者要怎么知道函数没有获取到文件呢?最简单的做法是返回空值。

func getConfig() {
 config := readConfigFromFile(CONFIG_PATH)
 if config == "" {
 config = DEFAULT_CONFIG
 }
}

等等,你的意思是说返回值就能让调用者知道函数内部发生了需要处理的错误,是吗?Hmmmm…… 我好奇为什么还会有人发明一整套混乱的机制欸?

你可能会说:“这不能强迫开发者解决问题,他们完全可以忽略错误而不做处理,而 Java 强制通过异常抛出机制要求程序员解决所有的异常,无论如何都是要通过 try ... catch 处理的,或者还有全局的错误拦截。总之 Java 的设计让程序员更容易编写健壮的软件!”

Hmmmm…… 我好奇这段代码有什么效果欸?

public static void main(String[] args) throws Exception {
 // 想怎么抛怎么抛!
}

这么做也没有任何后果,可以把错误一直往上抛,直到程序的入口方法。反正 Java 里的方法定义已经要写一堆 public static 了,多写几个 throws Exception 也不会觉得有什么区别吧?

你可能还会说:“异常抛出机制为错误处理提供了统一的解决方案,所有异常都基于 Exception 类,彼此还相互继承,更是通过 OOP 的机制让错误处理变得更灵活!”

Hmmmm…… 原来是 OOP 啊。说起来,Go 由于缺少继承,的确不算 OOP 语言,但我们从不少 Java 程序员也喜欢读的四人帮《 设计模式 》中得知,组合优先于继承,并且应该针对接口编程而不是针对实现编程。好的设计模式中很少见到继承,因为不方便复用,方便复用的是接口。

啊对,Go 语言也有统一的错误处理范式,但没有引入任何新机制,因为错误的全部定义只有三行代码:

type error interface {
 Error() string
}

所有实现了 Error() 函数的结构体都是错误值。于是我们回到 Rob Pike 说的「Errors are values」。如何让调用者明确知道函数内部发生了错误,而不只是恰好返回了空值呢?——再返回一个专门的错误值。

func getConfig() {
 config, err := readConfigFromFile(CONFIG_PATH)
 if err != nil {
 config = DEFAULT_CONFIG
 fmt.Println("config: using defaults because of error", err)
 }
}
func readConfigFromFile(path string) Config {
 if body, ok := readFile(string); ok {
 if config, ok := parseBody(body); ok {
 return config, nil
 } else {
 return nil, &errorString{"error parsing config file body"}
 }
 } else {
 return nil, &errorString{"error reading file"}
 }
}

这是通用的解决方案,尤其是用于有多种类型错误的情况。假设一个操作只有成功与否,而我们不关心失败的具体原因,只关心成功没有,那可不可以让错误处理变得更简单呢?当然,返回布尔值就好了,看到上面的 ok 了吗?它接收到的是函数的第二个返回值,在这个例子里,这第二个返回值不是 error 类型,而是 bool 类型。

如果没有接收函数的所有返回值,编译器就会报错:assignment mismatch。如果接受了错误值却没有使用,编译器就会报错:declared and not used。这两个错误信息都不是为错误处理而生的,而是本来就存在。Go 语言没有发明 unhandled exception 这种东西来要求程序员处理错误,编译器甚至不知道「错误」这个概念,它只知道有函数返回值没有被赋值给变量,只知道有变量被声明了但没有使用,而这对 Go 语言严格的编译器来说都是不可接受的。当然,和 Java 一样,这也有规避方案,比方说把错误赋值给 _,编译器就会认为这个变量不需要使用,通过检查。不管有没有专门的错误处理机制,错误都很容易被程序员忽略,不做处理,这本来就不应该是由编程语言本身强加给程序员的事情。就像 Rust 实际上并不是一门安全的语言,无论怎么样,就连 Cloudflare 的高级工程师也会滥用 unwrap()。Java 把 throw 做得这么难处理,还用 try ... catch 把代码的嵌套层次变得越来越深,反而是多此一举。

用接口统一错误类型,用函数的多返回值上报错误,用严格的编译器类型检查预防错误不被处理,这整条链路都没有引入任何新的机制。Go 的错误处理范式,竟然如此平庸和无趣。

“Don’t Panic”

Don’t use panic for normal error handling. Use error and multiple return values.

—— Go Wiki: Go Code Review Comments

Go 官方给出的常见代码审查评论中,有一条是 Don’t panic,意思是说不要用 panic() 做错误处理。这个内置函数会终止当前函数栈的执行,并且会让当前函数本身也成为一个 panic(),也终止当前函数的调用者的执行,panic 就会这样沿着函数调用栈一直往上,直到程序入口函数,最后终止程序,除非中途调用了 recover() 恢复程序。

「沿着函数调用栈往上」,这听起来很像 Java 的抛出机制。Java 的 Throwable 一旦被抛出,就会一直往上直到被 try ... catch 接住,如果没有被接住就会使程序终止——和 panic() 一样。你发现问题了吗?Exception 也是 Throwable,倘若异常没有被处理,也会使程序崩溃。这种情况却是 Go 想要避免的,所以 Go 建议程序员使用多返回值和 error 类型,而不是使用会使程序终止的 panic()

这再次印证了「异常情况」和「会使程序崩溃的情况」本来就是两个完全不同的东西,被 Java 硬塞到了 Throwable 里,就为了让它们共用本就不合理的抛出机制。异常情况仅仅是程序的一个分支条件,这个条件可以用布尔值或者 error 类型的值表示。如果没有好好处理这种分支条件,那么程序顶多出现意想不到的行为,不应该崩溃。Java 的设计却使得本应该成为分支条件的语句,直接终止了整个程序运行。

会使程序终止的情况不应该是可以预料的分支条件,比方说死锁、内存溢出和数组越界。Java 说一个合理的应用程序不应该抓住崩溃(Error),而 Go 提供了恢复崩溃(panic)的方法。

defer func() {
 if r := recover(); r != nil {
 fmt.Println("Recovered ", r)
 }
}()

panic("Panicking!")

defer 也不是专门为了处理异常而发明的新语法,它所做的仅仅是推迟代码执行。推迟到当前函数的最后,当然也在 panic() 之后(不过在 return 之前)。由于 defer 语句在 panic() 之后执行,就可以在 defer 中执行 recover(),使得 recover()panic() 之后执行,这样就恢复了崩溃。

由于 panic() 会自己沿着函数调用栈往上,所以不需要在相关链路中的每一个函数签名中都写上 throws Exception 之类的东西,只要在某个环节执行 recover() 就好了。

不过无论怎么说,panic() 都不如 error 常用,也不鼓励使用。

最后

质疑 if err != nil,理解 if err != nil,成为 if err != nil 的拥护者,然后狠狠嘲讽 Java。这便是艾尔特拉克魂断杰威幕之后身处岣琅的心路历程,回见了。


  1. Panic: 另译「运行时恐慌」 ↩︎

大脑充血 Vol.92

2026-08-03 07:57:43

我发现自己遇到「不测风云」,也就是那些难以预料的烦心事时,已经能够做到表面平静地发疯十分钟,然后该干嘛干嘛。这周济南总是莫名其妙地开始刮大风、下暴雨,虽然讨厌袜子被打湿的感觉,但包里常备着折叠伞,也还算从容不迫。

其实还有些家人和工作上的大小事,不过我现在更在乎自己刚刚从 MUJI 买的粘毛刷好不好用,今天有没有吃燕麦喝酸奶,明天早上应该读《第一个人》还是《现象学导论》。印象中不少「重要人物」常常把自己的生活外包给别人,以腾出时间关注那些更具商业价值的事情。而我呢?如果我每天回到家不能亲自用吸尘器把地板拖得一尘不染,听见灶台点燃的热水沸腾的声音,我都不知道自己怎么活下去。


吾仅悉落

MY SKYSCRAPER music cover

MY SKYSCRAPER

Nirosta Steel

复古,有种…… 既老派又年轻的奇妙听感。小众中透露着沉稳,但又没有很沉稳,甚至有些随意,大概可以这样总结。

最喜欢《LOST IN MUSIC》《MY NAME IS NIGHT》《FRESH FEELING》《SPECIAL WEAKNESS》,大概就是整张专辑篇中后段的部分。

作者如今六十多岁了,专辑是沉淀了几十年的音乐成果;还是酷儿,有几首歌据说是写给男友的(尽管我没听出来是哪几首);貌似还特别喜欢中国文化,挺好的,但是在音乐最后听到中式的敲锣打鼓和京剧唱腔实在是有些令人措手不及。

卡夫卡的手绘画作,一个坐在椅子上的男人,弯着腰,手臂放在膝盖上,这只手臂撑着头。这幅画的名字叫作 Der Denker(思考者)

Walking to a different town

And talking–what did I learn?

Talking to a different crowd

And walking–what did I learn?

What did I do today?

What did I do today?

What did I do today?


I was holding on for something special

Holding on for something new

Holding on!

——《SPECIAL WEAKNESS》


心重如毛

基因编辑技术走到哪一步了?

📻

最近这几天最大的新闻应该是接受基因编辑治疗的女孩意外离世的事件了。相关报道发表在《Science》上,大致死因是用于作为基因编辑载体的病毒引发了女孩的免疫反应,导致肝肾功能出现问题,女孩不久后就离世了。

一名六岁罕见病患儿,在上海接受前沿基因编辑试验后不幸离世,家属持续维权,要求厘清责任。消息从周五晚间开始在自媒体圈层扩散,随后逐步被官方媒体转载。截至录音当下,仅有上海交大医学院发布声明称启动调查,完整官方结论尚未公布。

《万物生长 FM》是讨论生命科学相关的播客,这档节目的主持人大多都是相关行业的从业者或研究人员,其中的一些对基因编辑技术也有涉猎。播客先是梳理和复盘了事件全貌,然后讨论了基因编辑疗法的现状,表达了他们对此事的看法。其中的不少观点,就目前来看,要比主流媒体更具专业性。

他们批评不少自媒体将此事件与贺建奎事件并列而谈的行为(据说这次事件的主要负责人仇子龙教授,曾经还声讨过贺建奎),因为贺建奎事件完全是私下进行的,他对婴儿进行基因编辑完全没有获得任何审批,再者,基因编辑的胚胎在发育之后,还会将被编辑过后的基因遗传给下一代,是更复杂的伦理问题。本次仇子龙事件,是经过必要的审批的,也就是“合规”的。所以,除了指责仇子龙本人,还应该反思的是整个审批的流程体系。据说基因编辑的药物在猴子身上试验时就出现了较强的负面反应,而相关机构在动物试验的毒理报告出来之前就通过了临床试验的审批,这很匪夷所思。仇子龙教授本人,除了接受女孩家长的研究资助之外,还收取了茅台酒、手机和平板电脑等赠礼。这两个关键点才是更值得关注的,整件事情都非常诡异且令人不安。

无论怎么说,目前所能看到的就只有《Science》的报道和各种媒体的转发和添油加醋,此外就是上海交通大学发布的「正在调查中」的声明。要更好地了解事件全貌,还要等待更多更可靠的信息来源发布报道。

有史以来最强的厄尔尼诺现象

📜

如果读者在高中学过地理的话,应该对厄尔尼诺现象这个词有所耳闻。每隔数年,太平洋东部的海水水温就会异常升高,导致一系列气候异常。温度上升往往和气压降低同时出现,东太平洋气压降低,空气就会从西太平洋往东太平洋流动,使得西太平洋沿岸地区(东南亚、澳大利亚)发生干旱,东太平洋沿岸(南美洲)发生暴雨,农业和渔业都会受到影响。

原本自然发生的气候异常,在今年创下新高。模型1的预测显示今年的厄尔尼诺现象会使得相关地区的海平面温度升高 3.6°C,而此前我们从未观测到超过 2.75°C 的厄尔尼诺,这次厄尔尼诺的最低数值 2.8°C 也超过了历史最高。

不同预测模型的结果有差异,但也都预告了前所未有的高增长。根据当前的 实时数据 来看,原本的 3.6°C 甚至上升到了 3.7°C,预计会在今年十一月达到这个峰值,而厄尔尼诺对全球气温的影响一般要到第二年才会完全显现,届时 2027 年可能超过 2024 年,成为有史以来最热的一年。


鸡头蛇尾

Tangled 是真正的 GitHub 替代品吗?

🐑

上周分享了 Codeberg 的政策变更 ,顺带明确了这样一个事实:Codeberg e.V. 作为非营利组织的使命是为自由软件提供基础设施,Codeberg 不是自由版本的 GitHub。如果用户在任何层面与 Codeberg 的理念不符,例如不以自由软件协议分发源代码、大量使用 LLM 写代码或者为 LLM 生态做贡献、存放实验性代码和不面向公众的个人项目,那就不应该待在这个平台。

政策变更其实也炸出了一部分不理解这个事实的用户,甚至有 一篇说自己后悔迁移到 Codeberg 的文章 登上 Hacker News 热门。好笑的是,文章里这个作者说 Codeberg 完全没有考虑过倾听用户的意见,然而,Codeberg e.V. 的决策相当透明且民主,我作为关心 Codeberg 和自由软件的捐献者,也早在一两个月前就了解到了相关议程。倘若这些用户真的在乎 Codeberg,那么他们完全有时间加入 Codeberg e.V. 去发表意见和投票。倘若在看到变更通知和首页横幅之前都没有想过去了解 Codeberg 的决策过程,那还能说些什么呢?

话说回来,事实就是,不是所有人都应该迁移到 Codeberg,而且绝对不应该把 Codeberg 当作 GitHub 的替代品。那真正合适的替代品是什么呢? GitLab 看起来只是另一个商业平台,只是不受微软控制,产品质量也没有像 GitHub 那样急速下滑,自托管的成本比较高,而官方实例的免费额度有限,更适合企业用户。 Forgejo 是更轻量的选择,不过自托管的不便之处,一是维护成本和门槛高,二是实例之间难以互通和协作。

还有一个小众且观点鲜明的选择是 Sourcehut ,不跟踪用户、没有广告、没有 JavaScript、没有 AI 功能、没有非必要的登录墙,你可以自托管也可以付费使用他们的服务。最低档是 4 美元一个月,但如果你因为经济状况和各种原因无法支付,也可以申请免费使用。整体而言,Sourcehut 的社区感似乎不强,界面对不少用户来说可能算得上丑(但完全可用)。功能上,Sourcehut 使用 git send-email 进行协作,交流、讨论和发送代码补丁都可以通过电子邮件进行,余下的 CI、工单管理和 Wiki 也都有,功能不会差。我怎么觉得我应该去用 Sourcehut?

和 Sourcehut 一样也还在 Alpha 阶段的另一个 Git Forge 是来自芬兰的 Tangled又名《魔发奇缘》,我在 第 79 期周刊 分享过,可以说是目前为止在体验上接近甚至超越 GitHub,也更适合大多数人的选择。他们的 Stacked Pull Requests (也就是把一个大的 PR 拆分成更容易审查的小块,但是可以一起合并),就在最近还被 GitHub 抄去用了 。他们对兼容 Git 的版本控制系统 Jujutsu 有直接的支持。几个月前他们还推出了 担保 功能,可以为那些能够交付可用且容易审查的代码的人(而不是滥用 LLM 的人)进行担保,受信任等级更高的人的贡献可以被优先处理。Tangled 的用户界面也很有特色,Commit 历史一般是被藏起来的,而他们把历史和当前的文件目录放在了一起。

不过更重要的是,Tangled 是去中心化的,不仅可以自托管,而且自托管之后能够通过 AT Protocol 与其他实例(Tangled 称作 Knot,也就是「结」)交互,比 Sourcehut 的电子邮件体验要丝滑不少。Tangled 背后的是 Tangled Labs Oy,一家芬兰的小公司,是位于欧盟的商业实体,无论如何都要比位于美国的微软更值得信任。

不过 Tangled 会不会在以后逐渐屎化(enshitificate),还有待观察。我注册了 账号 ,或许可以当备用 Forge 或者镜像,先用着看看。

Gopher SLIPs into a pile of parentheses

🐭

这个周末想写个小的 Web 应用,大概就是支持 Fever API 的无头 RSS 聚合器。我发现自己几乎从来不会打开 Miniflux 的 Web 界面,那为什么不能只用本地客户端,而服务端只起到同步的作用呢?就着这个想法,我准备开写。自己的项目当然是要用 Lisp 写的,不过用哪门 Lisp 呢?Clojure 我已经比较熟悉了,但跑在 JVM 上最小也得占两三百 MB,看着实在是有些不是滋味。于是选择了刚学没多久的 Common Lisp,然后就被 ASDF 和 Quicklisp 整得头疼——你这包到底应该怎么导?你这项目脚手架到底应该怎么搭?

我的确找到了不少合适的工具和教程,但整体上文档都很少,略感不适。在找 Web 框架的时候突然发现了 Caveman2 这个不洁的东西。

(defparameter *app* (make-instance '<app>))

@route GET "/"
(defun index ()
 (render #P"index.tmpl"))

What is this non-Lisp Java-like ungodly syntax!?

怒,骆驼被压垮了,遂离开 Common Lisp,开始物色新语言。毕竟是自己的个人项目,技术选型可以随意些,要不是非常想用 Lisp 写,我早就用 Go 糊出来了。我开始寻找宿主语言是 Go 的 Lisp,意外发现了今年年初才发布的新鲜语言 SLIP,名字的意思是 SLIce Processing。其中 Slice 是 Go 语言的数据结构,其实就是一个可变数组。SLIP 是基于 Slice 的,而不是 cons2,据说是出于性能考量。似乎好多 Lisp 都是用宿主语言的数组或者列表类型糊出来的,所以用 Slice 也并非不能接受,至少语法没有那么不洁

SLIP 基本上就是对 Common Lisp 的实现,仅仅有一些不兼容的地方。SLIP 还提供了插件系统,可以用 Go 拓展 SLIP 这门语言本身,自定义内置函数。我本来还想写一个插件简单包装下 Gin ,结果作者告诉我其实有内置 HTTP Server 。我还问了作者有没有考虑写更详细的文档,结果对方点醒了我,在 Common Lisp 里,用 REPL 探索语言才是最好的方式,apropos 函数可以搜索包和符号,而 describe 可以描述一个符号(函数)的作用。

嗯?所以搞了半天你不还是在写 Common Lisp 吗?甚至换了一个更难导包的实现。

于是这个周末就这么过去了,嗯……

杀死 Cookie 横幅

ℹ️

欧盟有关 GDPR 的立法运动,网站声称 Cookie 横幅(也就是访问某些网站时会弹出来的,请求你同意 Cookie 协议的横幅)往往被设计得极具误导性,用户要真正表达自己的隐私偏好会很困难,很有可能就一不小心同意了网站跟踪你。同意所有 Cookies 很简单,接受必要的 Cookies 很简单(但谁知道「必要」究竟是指什么呢?),但拒绝 Cookies 往往不那么容易。这个网站提供的解决方案是,让用户在操作系统或浏览器层面设置全局的隐私偏好,与网站建立连接时,就像 HTTP Headers 里的 Accept-Language 字段一样,自动告知网站访客的隐私偏好。

这的确是更好的解决方案,如果默认设置了严格的隐私偏好,网站就不太可能通过误导性的 Cookie 横幅来引诱用户同意交出部分权利。

另外,点击网站的 Take Action 按钮,会来到一个引导页面,告诉用户采取行动的方式就是联系当地的欧洲议会代表人,让他们支持 Article 88b GDPR3,之后还列出了各个国家和各个地区的联系人信息。不得不说,真是方便。


漫步遐思

This is a thoughtless week.


  1. 这里的模型不是指 AI 大语言模型,而是基于数理统计的计算模型。 ↩︎

  2. 关于 cons 是什么,我在《 Just A Common Lisper 》里写过。 ↩︎

  3. Article 可能是「法案」的意思?但对应的单词还有一个是 Bill。我不清楚法律术语,所以就用原文了。 ↩︎

川渝人在山东吃到没有辣味的麻辣香锅和红油水饺之后产生的哲学思考

2026-07-31 23:53:51

我没有什么乡愁,胃也是,就算不辣也有很多好吃的。只是那些听起来辣,看起来也辣的东西,吃起来竟然不辣,令我震惊,故作此文。

来济南的第一天,到了酒店之后还有些晕,毕竟刚下飞机就又在车上坐了接近一个小时,不想出门,于是选择点外卖。纠结过后,看上一家麻辣香锅,习惯性地点了微辣,打开饭盒品尝,没有辣味。第二天,我不服,我就想吃有辣味的麻辣香锅,于是点了中辣,打开饭盒品尝,仍然没有辣味。几天后我仍对此耿耿于怀,于是点了最辣的麻辣香锅,打开饭盒品尝…… 这在重庆应该叫作微微辣吧?

虽然不辣,但第二天却拉肚子了,而且是那种吃了辣之后伴随着灼烧感的拉(我是不是应该在文章开头标注这篇文章不适合吃饭时阅读?但真的有人会一边吃饭一边读东西吗?)。真是神奇,原来辣度和能让人拉肚子的程度是分开的吗?

一星期过去,我已经习惯了济南餐馆里标注了辣味的食物实际上并没有辣味这个事实,于是毫无戒备地点了一份中辣的重庆鸡公煲…… 被辣出了感动的泪水。之后又去吃海底捞,点了海底捞双料真香锅,很香很辣很正宗。就这样,我对济南的信心又回来了。然后我站起身,路过好几张桌子…… 不是,你们为什么用开水烫火锅?愤恨中想要告诫对方,吃火锅要是没有小孩,点鸳鸯锅是会被开除重庆户籍的,点清水锅更是要被诛九族。愤恨之余想起来,哎呀,自己是自闭症来着,不敢和人讲话。(想起来的不应该是你不在重庆这个事实吗?!)

周末就这么过去了,到了周一,在公司楼下吃饺子。蘸料区有醋、酱油、耗油和辣椒,最佳的配比当然是 0 份醋、0 份酱油、0 份耗油和 10 倍的辣椒。可惜,这个辣椒恐怕当饮料喝也没有辣味。我把饺子在蘸碟里搅来搅去,让它沾满辣椒油,入口之后,没有辣味,不过挺香的。

所以,在济南,香辣等于香,麻辣等于麻,甜辣等于甜。原来这就是合并同类项啊!嗯?等等,难道说应该叫奥卡姆剃刀?你究竟在谈数学还是哲学?!不是在谈吃饭吗?

好啦好啦,玩笑结束,我知道部分地区的饮食文化更推崇品尝食材本身的味道而不是搭配调料,或者就算加调料也更偏好酱香而不是辣味。不过我依旧有一个疑问:那不辣的麻辣香锅和不辣的红油水饺究竟有什么受众呢?

总所周知红烧牛肉面对于一部分人来说其实是辣的,麻辣香锅想必同理。

—— HPCesia

啊,原来是这样吗?我确实一直不太喜欢红烧牛肉面,因为味道平平淡淡,毫无特色,据说一些不能吃辣的人会觉得红烧牛肉面太辣呢。所以,真正的问题是:那红烧牛肉面究竟有什么受众?

以后出门吃饭要随身携带老干妈。等着我把超意兴的把子肉涂满辣子吧。

回到正题上来。人们常说辣是痛觉而不是味觉,事实也的确如此。

辣是化学物质(譬如辣椒素、姜酮等)刺激细胞,在大脑中形成了类似于灼烧的微量刺激的感觉,不是由味蕾所感受到的味觉,而是疼痛。

—— 辣 - 维基百科

据说人体在受到能忍受的疼痛时,会释放内啡肽(endorphin)。这种快乐激素的化学结构和作用类似止痛药吗啡(morphine),但因为在人体内分泌,所以叫作内源性吗啡(endogenous morphine),英语单词缩合之后变成 endorphin。为了缓解疼痛,所以产生止痛物质,继而使疼痛本身带来了快感——无论是辣椒素的灼烧感还是…… 别的什么…… 奇怪的东西

吃辣带来的快感很大程度上是因疼痛带来的内啡肽造成的,那么不辣的「辣味」食物呢?对于已经能够耐受相当程度的辣的川渝人来说,刺激感较弱的辣貌似并不能带来足够的内啡肽分泌,即便如此,沾了辣油的食物和不沾辣油的食物仍然有区别。

我想这是因为辣往往伴随着其他的风味出现,我们可以从火锅底料的配方看出些许端倪。诚然,火锅底料大部分都是油和辣椒(毫不夸张地讲,熬火锅底料的锅里一眼看去就只有辣椒,而且还是那种铁桶一样大的锅……),但仍有不少其他佐料。我简单搜索之后,发现了一种牛油火锅底料的 专利信息 ,以下是摘要,可以用作参考:

本发明公开了一种牛油火锅底料配方,以牛油、郫县豆瓣、白酒、醪糟、糍粑辣椒、生姜、大蒜、花椒、豆豉、冰糖、辣椒面、大葱为主料,以白蔻、草果、丁香、砂仁、孜然、桂皮、甘草、栀子、老蔻、甘松、陈皮、香茅草、八角、香叶、千里香、小茴香、香草为香料,该发明口味浓郁厚重,可以满足食客对火锅口味的需求,并适合在潮湿阴冷地区或者冬季食用。

所以,除了辣椒和油之外,口味浓郁的辣味食物往往还带有不少香料的风味。我想那些不够辣的辣椒油吃起来还有些滋味的原因也是如此。

作为对比,有一种很直接的辣,或许被称作「工业辣」,基本没有什么香味,只有并不愉悦的痛感(我猜成分可能是很纯的辣椒素)。去一些食品安全状况存疑的餐馆用餐时偶尔会有这种辣的体验,吃火鸡面也会有。是的,我觉得火鸡面太辣了,而且辣得很没有品味。YouTube 上有一个频道做的一系列节目貌似很有名,叫作 Hot Ones ,节目大概是请一些明星一边吃辣度不同的鸡翅,一边回答问题。尽管我没有尝试过视频里出现过的那种辣酱,但印象中那种食物也很「工业」,而且貌似就是被设计来给人「体验」的,故意把辣度拉得很夸张。

人们好像很喜欢看别人被辣得说不出话,展现出某种人皆有之的弱点,或是反过来,故意吃辣度很高的食物,来证明自己的承受能力很强。等等,我刚刚是不是写过「没小孩在,点鸳鸯锅会被开除重庆户籍」?原来我也被拉进这场无聊的游戏里了啊,真是可怕。

我想起之前树老师在《 真男人为什么应该练翘臀? 》里批评过网上那些信口开河说自己每天早上起来都能做一百个俯卧撑的男人。那些男人大多数(或许是全部)都在撒谎,因为现实中肌肉力量远超常人的美军陆战队退役大哥,做连续不间断标准俯卧撑,连续最多只能做七十多个。说自己早上爬起来就能做一百个的,要么是在撒谎,要么是动作不标准,要么是分组做的(分组是很好的练习方式,但要放到网上显摆的话,谁知道你组间休息多长,每组多少个?)。

我印象中大多数人的俯卧撑姿势都有问题,却从没反思过动作问题,哪怕不标准也要算进去。事实上刚健身的人,连续不间断做十个标准俯卧撑就已经很难了,刚开始更是只能推墙或者做跪姿俯卧撑。我不知道这种愚蠢的雄竞和男性身体焦虑从何而来,但似乎在小学体育课上就有显现。学不高身不正的体育老师用成人的标准(而且是有健身基础的成人标准)傲慢地要求小孩子做标准的连续俯卧撑,仿佛做不了才是不正常的,而且完全忽视个体的身体情况差异。于是大家都开始无知地卷了起来,无知是说大家以为把手臂弯一弯就算俯卧撑,能弯二十下是正常的,而上肢力量更弱的人,可以跪着做(我印象非常深刻,我小学体育课上的跪姿俯卧撑也不是标准的跪姿,反而更像瑜伽动作里的猫牛式);卷是说所有人都开始以一个不存在的人为标准,这个人能连着做几十个俯卧撑不喘气,每天只吃水煮菜,早上起来会洗冷水澡…… 树老师说那些即便骗人也要说自己能做 100 个俯卧撑,并且说男人都能做 100 个这种话给别人制造焦虑、让自己看起来很「男人」的男人,实际上在供奉某个不存在的「赛博关公」。我想说,在互联网之前,这种供奉就已经持续很久了。

加缪在他的自传体小说《第一个人》里用两个词描述他的外婆——无知且严厉。她会非常自信地用歪理教育孩子,严厉不是为了做正确的事,而是为了让孩子顺应她无知的头脑所能认知到的规律。这类人还很好面子,加缪写道,他小时候很不喜欢去电影院,因为那时候的无声电影要看字幕,她的外婆不认字,要外孙读字幕,却还要在开始前大声说「我今天没戴眼镜,你给我读字幕」,只为了不让人发现她是文盲。结果是,这个可怜的小男孩一边被旁边恼怒的观众责骂太吵,一边因为念得太慢被外婆催促,有好几次都被赶出了电影院,而男孩又因为浪费了家里的钱继续感到自责。

这类人不少见,有可能许多人的父母长辈都是如此。被无知且严厉的养育者带大的孩子,若不能通过读书或社交自救,恐怕就会成为焦虑和不存在的标准的奴隶,而他们也会继续想办法大声说一些谎话,来证明自己不是文盲、弱鸡和不能吃辣的人,也不知道是证明给谁看。

不识字也可以有智慧,苏格拉底甚至反对书写,认为那会让人们放弃记忆;识字的人也不见得能品味文学和阅读哲学,甚至不能读懂别人写的字,不能用文字去做有意义的事情。「弱鸡」仅仅是缺少力量,并不是没有勇气;只有力量却不懂得克制使用力量的人,并不具有美德。不辣也有很多美食;仅仅是提高辣度,而忽略了风味,把吃辣当作某种游戏,那实在是有些不尊重食物。

就这样,我得去看看什么样的辣椒酱比较便携了。

Are We Interfacing Yet?

2026-07-28 00:03:22

我在自己的时间里一直坚持手写代码,但工作时难免与 Agents 打交道。一方面是公司推崇这种工具,另一方面是如果我不用的话,我就没办法按时交付工作。无论如何,有一类代码我在任何情况下都是自己设计和自己手写的,那就是接口定义。

我要讨论的不是 RESTful API,也不是 gRPC 的 Protobuf,我说的是 interface 类型。不同语言中的 interface 都略有不同,但一般都是一系列抽象方法(函数)的集合。抽象方法是说这个方法没有被实现,仅仅定义了函数名、形参数量和类型,也就是函数签名。

interface 一般用在面向对象编程中,实现 interface 就是定义一个类或命名空间,在其中实现 interface 定义的所有函数。在 Java 中,实现接口需要显式声明,例如 class ConcreteWriter implements Writer。在 Go 中,实现接口是隐式的,只要某个结构体的方法包含了某个接口定义的所有方法,就视作实现了这个接口。

动态类型语言中也有 interface,不过较少使用,我想原因是 interface 就是为静态类型系统服务的。实现了接口的某个对象可被视作同一个类型,而架构设计中的一个原则是:依赖关系应该向越来越抽象的方向流动,即抽象的不该依赖具体的、具体的应当依赖抽象的。原因在于,具体的软件本身是易变的(关于易变性带来的复杂性,可以阅读《 什么是工程问题? 》),如果抽象的业务逻辑依赖具体的部分,那么具体实现改变之后,业务逻辑也要频繁改变,这显然是不合理的。软件工程的一个重要问题就是把变更控制在软件的一小部分当中,interface 可以做到。

假设我们要编写的软件需要启动一个浏览器实例做某个操作,比起直接在主业务逻辑中添加寻找 Chrome 二进制文件的路径、管理生命周期、发送请求和解析响应体的逻辑,把这部分操作隔离开来是最好的。一般的做法是这样:

func doBusinessLogic() {
 // ...

 url := parseFromUserInput(input)

 chrome := NewChromeInstance()
 chrome.Start()
 defer chrome.Close()

 page := chrome.GetPage(url)

 // ...
}

实际上这也不是最好的做法,如果我们某天决定不用 Chrome,改用 Firefox,那怎么办?我们要修改这个文件里的依赖引入,把 NewChromeInstance() 改成 NewFirefoxInstance(),假设 Firefox 没有 Close() 方法,用的是 Stop() 这个名字,也要修改。更何况,不同的浏览器可能还有不同的注意事项,需要额外增删代码。

你可能想用适配器设计模式(Adapter Pattern),但既然都要写新的类定义了,为什么不直接写成接口呢?

type Browser interface {
 Start()
 Stop()
 GetPage(url string) PageResult
}

修改前面的 doBusinessLogic 函数,不要直接使用 Chrome 或者 Firefox,而是依赖这个抽象接口。

func doBusinessLogic(browser Browser) {
 // ...

 url := parseFromUserInput(input)

 browser.Start()
 defer browser.Close()

 page := browser.GetPage(url)

 // ...
}

抽象的就是稳定的,Browser 大概率在未来仍然只会有启动、停止和获取页面这三个方法,即便我们替换了背后的具体实现,业务逻辑也不需要做任何改变,只要 ChromeFirefox 和未来所有的浏览器类实现了上述三个方法,并且符合里氏替换原则。

具体怎么做呢,假设我们在 main.go 里初始化环境、读取配置文件、启动必要的服务,也包括业务逻辑。你发现我在 doBusinessLogic() 的函数签名里加了一个形参吗?我们只需要在入口文件里初始化浏览器,然后把浏览器传给 doBusinessLogic()

func main() {
 firefox := NewFirefoxInstance()
 doBusinessLogic(firefox)
}
func main() { 
 chrome := NewChromeInstance()
 doBusinessLogic(chrome)
}

至于 NewFirefoxInstance()NewChromeInstance(),这两个函数里面可能封装了完全不同的初始化逻辑。有可能 Firefox 这边使用 Selenium 和 WebDriver 打开了一个受控制的浏览器实例,而 Chrome 这边用 chromedp 启动了资源占用更低的无头浏览器。要把什么样的具体实现喂给业务逻辑都没关系,只要实现了 Browser 接口,就能通过类型检查

Start()Stop()GetPage() 封装什么样的代码都没有问题,它可以老老实实地在本地分配内存、维护上下文、启动并管理一个浏览器进程的生命周期,然后与它通信。

type Chrome struct {}

func (c *chrome) Start() {
 // 分配内存
 // 以无头模式启动 Chrome 
 // 保留进程 ID
 // ...
}

func (c *chrome) Stop() {
 // 杀死进程
 // 回收资源
}

func (c *chrome) GetPage(url string) PageResult {
 // 与进程建立连接
 // chrome.Navigate(url)
 // 等待页面渲染完成
 // 返回页面结果
}

还能有什么方法呢?我们可以偷偷把 Start() 改为与某个远程服务器建立连接,用 Stop() 断开连接,而 GetPage() 则是向该服务器发送 HTTP 请求并解析响应体,然后封装成 PageResult 类型的结果返回,就把它命名为 RemoteBrowser 吧。显然,无论具体实现如何,RemoteBrowser 的确实现了 Browser 接口,可以直接扔进 doBusinessLogic() 函数里。

如此一来,我们的业务逻辑就在完全没有感知的情况下与另一个外部服务进行 HTTP 通信了。可见无论是网络通信、本地进程内部通信还是普通的函数调用,都只是技术细节,可以随时替换。

那外部服务器要怎么设计?会不会很麻烦?不会,我们不是已经把 Chrome 的具体实现写好了吗?假设我们刚才的代码全都放在 internal/browser 包里,只需要创建 cmd/server 包,让这个包依赖 internal/browser,复用 Chrome 的代码,再实现 HTTP 层的通信就好了。这些全都可以写在一个代码库里,只需要单独编译 cmd/server,把它部署到另一台机器上就好了。毕竟这是 Go,几分钟就可以编译好二进制文件,几行命令就可以把这个二进制文件传送到远程服务器上运行。

我们还可以再加上一层抽象,看看下面这个 BrowserCluster 接口。

type BrowserCluster interface {
 Add(browser Browser)
 Delegate(url string) PageResult
}

Delegate() 的意思是委托,它的形参和返回值与 Browser.GetPage() 一模一样。我设想的是,Delegate() 函数遍历 BrowserCluster 保存的所有浏览器实例,找出当前第一个空闲的浏览器,把任务委派给它,也就是调用它的 GetPage() 方法。

显然我们需要类似 Browser.IsAvailable() 的函数检查浏览器是否可用,我们要动手改 Browser 接口的定义吗?

不,修改 Browser 接口意味着之前所有实现了这个接口的结构体,无论 Chrome 还是 Firefox,都无法再通过类型检查(因为它们不再是 Browser 了,缺少 IsAvailable() 方法)。此时可以用到组合复用原则,也就是再写一个接口。

type BrowserResource interface {
 IsAvailable() bool
}

BrowserCluster 的具体实现可以依赖 BrowserResource 的抽象接口,如果需要的话,ChromeFirefox 可以选择性地实现这个接口,也就是添加 IsAvailable() 方法。我们来实现 BrowserCluster 试试看。假设这个浏览器集群不限值类型,只要是浏览器都可以放进去,那就命名为 AnyBrowserCluster 好了。

type AnyBrowserCluster struct {
 browsers []Browser
}

func (a *AnyBrowserCluster) Add(browser Browser) {
 a.browsers = append(a.browsers, browser)
}

func (a *AnyBrowserCluster) Delegate(url string) PageResult {
 for _, browser := range a.browsers {
 if resource, ok := browser.(BrowserResource); ok {
 if resource.IsAvailable() {
 return browser.GetPage(url)
 }
 }
 }
 return PageResult{}
}

AnyBrowserCluster 没有使用浏览器的 Start()Stop() 方法,光从接口隔离原则(ISP)的角度来看,这段代码不太合格,因为我们不该依赖自己不使用的代码。我们只使用了 BrowserGetPage() 接口,更好的隔离是把 Browser 拆成 StarterStopperPageGetter 三个接口,让 AnyBrowserCluster 只依赖 PageGetterBrowserResource,因为我们只用到了 PageGetter.GetPage()BrowserResource.IsAvailable() 方法。

不过,那样就有些复杂了,而且代码读起来也不那么顺畅。无论怎么说,stopper.Stop() 看起来都有点过于抽象了(双重意义上的),设计不可能同时遵循所有的设计原则。况且,我们很有可能需要在 AnyBrowserCluster 中管理浏览器的生命周期,比方说添加浏览器之后、或者在浏览器第一次被委派任务时,启动浏览器;在 AnyBrowserCluster 自身被摧毁时,遍历 browsers 调用它们的 Stop() 方法。上面仅仅是简化版的代码。

回到业务逻辑,把 Browser 换成 BrowserCluster

func doBusinessLogic(cluster BrowserCluster) {
 // ...

 url := parseFromUserInput(input)
 page := cluster.Delegate(url)

 // ...
}

再回到入口函数,让我们创建几个浏览器实例,放进集群里,然后把集群交给业务逻辑。

func main() {
 cluster := NewAnyBrowserCluster()
 cluster.Add(NewFirefoxInstance())
 cluster.Add(NewChromeInstance())
 cluster.Add(NewRemoteBrowser("https://imabrowser.dev:8081"))
 cluster.Add(NewRemoteBrowser("https://imabrowser.dev:8084"))
 cluster.Add(NewRemoteBrowser("https://imabrowser.dev:8082"))

 doBusinessLogic(cluster)
}

Voilà!这下业务逻辑就毫无感知地把资源消耗较大的任务委派给了当前空闲的浏览器,这个浏览器可能跑在本地,也可能跑在远程服务器上;可能是 Firefox 也可能是 Chrome,还可以是未来任何要添加到 cluster 当中的 Browser 实现。业务逻辑不知道自己用的是什么类型的集群,集群也不知道自己用的是什么类型的浏览器,它们仅仅是调用自己所依赖的接口实现的方法而已。任何一个环节需要改动,只需要修改那一部分的代码就好,变更不会扩散。

其实我们完全可以用 AnyBrowserCluster 实现 Browser,毕竟 GetPage()Delegate() 的形参和返回值一模一样。那样的话,doBusinessLogic() 就完全不用修改,业务逻辑仍然以为自己用的是普通浏览器,只有入口函数知道它喂给业务逻辑的实际上是个浏览器集群。

几乎任何具体实现都可以稍作修改,或者加上一个包装层来实现某接口,把它接到任何适配接口的地方。总之,接口让静态类型语言变得非常灵活,如果没有 interface,我可能完全不想离开动态类型语言的怀抱!

最后, 接口越大,抽象越弱 。如果你在用 Go 写代码,记得写小接口,不要跟 Java 程序员学坏了!