<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>西贝的代码宇宙</title><description>在星辰与字节之间，写下一行属于宇宙的代码。</description><link>https://mrsibe.top/</link><language>zh_CN</language><item><title>谈谈我的学习工作流</title><link>https://mrsibe.top/posts/learning-workflow/</link><guid isPermaLink="true">https://mrsibe.top/posts/learning-workflow/</guid><description>从信息来源、信息消化到信息回顾，分享我上大学以来摸索出的学习工作流：借助一手资料和四问方法理解新知识，通过主动回忆与博客输出消化信息，再在实践中不断回顾和修正。</description><pubDate>Sat, 10 Oct 2026 08:52:41 GMT</pubDate><content:encoded>&lt;h1&gt;谈谈我的学习工作流&lt;/h1&gt;
&lt;p&gt;计算机这个行业，每天都有新的技术或者产品诞生，海量的前沿信息经常涌入我的社交媒体软件，让人应接不暇。更何况这些信息还要进入自己的大脑，甚至过一段时间会被遗忘。因此，形成一套学习工作流，让自己记录信息，可以随时阅读复习非常重要。下面介绍一下我上大学以来逐渐摸索出来的工作流。&lt;/p&gt;
&lt;p&gt;我的学习工作流大体分为三个部分：信息来源、信息消化、信息回顾。&lt;/p&gt;
&lt;h2&gt;信息的来源&lt;/h2&gt;
&lt;p&gt;关于信息的来源就比较多了。我平时会刷小红书、知乎、Reddit、X 等社交平台，每个平台得到的信息有所不同。比如说小红书得到的信息比较全面，而知乎的贴子观点会比较深入，Reddit 主要获取社区的舆论，X 则是最快获取前沿信息的地方。新出现一个新的事物，可以去各个平台都刷刷，往往会得到不同的见解。&lt;/p&gt;
&lt;p&gt;但是不管怎么样，这些信息都经过其他作者的加工，往往会带有主观的见解，甚至误人子弟。想要获取客观的信息，最好去看一手资料。比如说，新出了一个模型，就去看它的官方博客、权威的排名榜单；新出了一个开源项目，就去看看它的源代码或者文档。&lt;/p&gt;
&lt;p&gt;有些时候，比如说我要调研一个行业，但是搜索到的信息会太多，找不到调研的切入点。好在现在有大语言模型，它能帮我做调研，给我一个切入这个知识点的路径。&lt;/p&gt;
&lt;p&gt;我获取信息的工作流一般是：先通过社交媒体获取最新的资讯，如果刷到感兴趣的，就让 ChatGPT 帮我调研一下这个东西，写个类似于综述一样的东西让我阅读。然后我就通过“四问方法”自己亲自去搞懂这个东西（所谓四问，就是 What、Why、How、Trade-off，你可以在我&lt;a href=&quot;https://mrsibe.top/posts/independent-thinking/&quot;&gt;关于如何独立思考的文章&lt;/a&gt;深入了解）。最后，我会不断地刷社交媒体，结合别人的见解看看自己的观点还有没有没有考虑到的地方，逐步完善。&lt;/p&gt;
&lt;p&gt;当然，并不是每一条信息都值得花时间深入研究。我一般会先判断它是否与自己的学习方向有关，或者是否能够解决当前遇到的问题。对于那些只是有趣、但暂时用不上的信息，我不会强迫自己消化，不然反而会成为信息负担。&lt;/p&gt;
&lt;h2&gt;信息的消化&lt;/h2&gt;
&lt;p&gt;信息的消化主要是记笔记。我在高中的时候总是觉得，知识都记在脑子里了，为什么还要记笔记；到后面才发现，其实记笔记是为了当你遗忘一个知识点的时候，能迅速回看笔记，唤醒自己的记忆，而不是重新调研、重新学习。&lt;/p&gt;
&lt;p&gt;消化信息还需要主动回忆。我们平时都是看着资料记笔记，这样记笔记实际上很容易变成资料抄写，而不是记录在脑子里面。主动回忆则是，看一遍资料，关掉资料，凭借自己脑海的回忆写出笔记，再打开资料给自己的笔记补充和纠错。&lt;/p&gt;
&lt;p&gt;通过输出内容倒逼自己消化信息。我会把自己的笔记转化为博客发布在各个平台上。这一步其实相当重要，因为笔记是给自己看的，博客则是写给别人的。写笔记时，我只需要确保自己能够理解；但写博客时，我必须站在读者的角度，重新组织知识，解释概念之间的联系。在这个过程中，我经常会发现一些原本以为自己理解了、实际上却解释不清楚的问题。这也促使我重新查阅资料，进一步完善自己的认知。&lt;/p&gt;
&lt;h2&gt;信息的回顾&lt;/h2&gt;
&lt;p&gt;学习终究不是为了学习本身，而是为了能够在实践中运用知识。反过来，在实践的过程中，我们也很可能对原有的知识产生新的理解。因此，笔记并不是写完就可以扔在一边的东西，而是需要不断回顾、补充和修正。&lt;/p&gt;
&lt;p&gt;对于那些重要的基础知识，我会通过主动回忆的方式定期复习，尽量让它们留在自己的脑子里。但并不是所有知识都值得记住，比如说一些不常用的 Linux 命令或者框架配置，我通常只需要知道它们在哪里，等到实际需要的时候再翻看笔记。同时，随着实践经验的积累，我也会不断修正自己的笔记。因为学习并不是一个线性的过程，今天理解的知识，过了一段时间再回头看，可能会有完全不同的认识。&lt;/p&gt;
&lt;p&gt;这样一来，学习就形成了一个循环：获取信息、消化知识、实践应用、回顾修正，再产生新的理解。 随着这个循环不断进行，自己的知识体系也会逐渐完善。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;建立这套学习工作流其实只是为了应对两个很简单的问题：信息太多，以及学过的东西容易遗忘。但学习的目的并不是收集尽可能多的资料，也不是让自己的笔记越来越厚。真正重要的是，我们能否理解这些知识，并在需要的时候运用它们。当然，这套工作流也并不完善，很多地方还在不断调整。它只是我上大学以来慢慢摸索出来的一套方法，不一定适合所有人。如果你也有自己的学习方法，欢迎一起交流。&lt;/p&gt;
</content:encoded></item><item><title>如何不被自媒体干扰，真正地独立思考</title><link>https://mrsibe.top/posts/independent-thinking/</link><guid isPermaLink="true">https://mrsibe.top/posts/independent-thinking/</guid><description>面对自媒体带节奏的热点信息，如何用 What、Why、How、Trade-off 四个问题先建立自己的判断，再用外部评价检验和修正，避免在情绪和立场先行时被牵着走。</description><pubDate>Thu, 08 Oct 2026 16:12:00 GMT</pubDate><content:encoded>&lt;p&gt;不论是刷小红书还是刷 X，我们总是能看到各种热点新闻：“小龙虾风靡全球”“ChatGPT 发布史上最强模型”“某某模型打败了所有竞争对手”……现在的信息传播非常快，一个新产品刚刚发布，可能还没来得及仔细了解它，互联网上就已经出现了成百上千条评价。有人吹捧，有人批评，也有人忙着宣布某个行业即将被颠覆。&lt;/p&gt;
&lt;p&gt;媒体当然有存在的必要。它们让我们能够以很低的成本获取大量信息，了解世界正在发生什么。但问题在于，越来越多的自媒体为了获取流量，往往会在客观事件上附加大量主观评论，甚至夸大事实、制造对立、标题党。最让我困扰的并不是这些内容本身，而是：&lt;strong&gt;当我们长期接触这些带有强烈情绪的信息时，是否还能对一个事物形成自己的判断？&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;也许你一开始就被干扰了&lt;/h2&gt;
&lt;p&gt;以前刷帖子的时候，我经常会看到一些诸如“震惊瘫坐”“彻底封神”的内容。点进去一看，某个 KOL 把某个新产品吹得天花乱坠。继续往下刷，又是一堆类似的评价，看得多了，我很可能不自觉地接受这些观点并得出一个结论：“XXX 确实封神了。”&lt;/p&gt;
&lt;p&gt;我认为这种获取信息的方式有两个问题：首先，人很容易受到第一印象的影响。如果第一次接触某个事物时，就有人告诉你“它很厉害”或者“它就是垃圾”，这个评价很可能成为你后续判断的锚点。其次，自媒体本身就包含大量经过筛选和加工的信息。一个新模型发布的时候可能只展示它最优秀的测试成绩，而忽略它在其他任务上的不足。这意味着，从信息获取的源头开始，我们就可能已经被干扰了。更麻烦的是，很多时候我们并不会意识到这种影响，觉得自己是在独立思考，实际上我们只是在复述别人的观点。&lt;/p&gt;
&lt;h2&gt;暂时屏蔽外部评价，用四个问题认识事物&lt;/h2&gt;
&lt;p&gt;面对一个新事物，我认为应该暂时放下外部评价，先从客观事实出发，建立自己的理解，再参考他人的观点修正判断。在建立自己的理解的时候，我通常会想办法回答下面四个问题：&lt;/p&gt;
&lt;h3&gt;What：它究竟是什么？&lt;/h3&gt;
&lt;p&gt;比如说，当我们看到“某某 AI 模型彻底超越了 GPT”时，首先应该弄清楚它究竟发布了什么，所谓的“超越”体现在哪些指标上，数据又来自哪里，而不是直接接受媒体的评价。因此，获取信息时应该尽可能回到一手资料。研究开源项目，可以查看 README、官方文档和源代码；研究新模型，可以阅读技术报告、评测结果并尝试实际使用。当然，一手资料也未必完全客观，但相比经过多次转述的信息，至少能够减少一些不必要的干扰。&lt;/p&gt;
&lt;h3&gt;Why：它为什么会出现？&lt;/h3&gt;
&lt;p&gt;理解一个事物还需要分析它出现的背景，以及它试图解决的问题。&lt;/p&gt;
&lt;p&gt;以 Docker 为例，表面上它是一套容器技术，可以用来打包和运行应用。但如果继续追问为什么需要 Docker，就会发现传统的软件部署存在环境不一致的问题：开发环境与生产环境的依赖、配置可能不同，导致应用无法稳定运行。Docker 通过容器镜像等机制，使应用及其依赖能够更加一致地交付和运行。&lt;/p&gt;
&lt;p&gt;这样的思考方式不仅适用于技术领域。无论是一个新产品、一种商业模式，还是某个突然兴起的行业，都可以追问：它解决了什么问题？过去人们如何解决？原有方案存在哪些不足？&lt;/p&gt;
&lt;h3&gt;How：它通过什么机制实现？&lt;/h3&gt;
&lt;p&gt;理解一个事物还需要进一步研究它的工作原理，搞清楚它是如何解决问题的。&lt;/p&gt;
&lt;p&gt;还是以 Docker 为例，理解了环境一致性的需求之后，就可以继续研究镜像的分层机制、Namespace、Cgroups，以及容器与虚拟机的区别。这样才能明白 Docker 为什么能够提供相对隔离、可重复的运行环境，以及它为什么不等同于虚拟机。&lt;/p&gt;
&lt;p&gt;这个问题的关键在于，不要只关注它能够做到什么，还应该理解它为什么能够做到。尤其是面对自媒体宣称的技术突破时，只有了解其实现机制，才能判断哪些是真正的创新，哪些只是对已有技术的重新包装。&lt;/p&gt;
&lt;h3&gt;Trade-off：它获得了什么，又牺牲了什么？&lt;/h3&gt;
&lt;p&gt;任何技术、产品和商业模式都不是非黑即白，而是有其优势和劣势的。“Electron 是不是垃圾”“Rust 和 Go 哪个更好”“k8s 值不值得使用”，这些问题其实都缺少具体的评价标准，是没有太多意义的。&lt;/p&gt;
&lt;p&gt;比如说，Electron 可以利用成熟的 Web 技术快速开发跨平台桌面应用，但代价是相对较高的资源占用和安装包体积。Kubernetes 提供了强大的容器编排能力，同时也引入了额外的学习和运维成本。它们本身并没有绝对的好坏，关键在于获得的收益能否覆盖付出的代价。因此，评价一个事物时，应该明确它相比现有方案改善了什么，又引入了哪些新的问题。&lt;strong&gt;脱离成本和收益谈优劣，很难得出有价值的结论。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;外部的评价应该负责修正自己的思考&lt;/h2&gt;
&lt;p&gt;独立思考并不意味着拒绝外部评价，专业人士的经验、社区讨论和媒体报道仍然具有很高的信息价值，关键在于我们如何使用这些信息。我认为应该先用上面提到的四问方法建立自己的初步判断，再阅读别人的评价来检验自己的判断是否还有没有思考到的地方。&lt;/p&gt;
&lt;p&gt;阅读外部评价时，还需要注意区分事实与观点。一个产品在某项测试中取得了较好的成绩，是事实；认为它因此成为“最好的产品”，则属于评价。即使某条帖子获得了数千次点赞，也只能说明它引起了广泛关注，不能证明其结论正确。&lt;/p&gt;
&lt;p&gt;此外，还应该主动寻找反对自己观点的证据。如果认为某项技术不好，可以研究为什么仍有大量开发者使用它；如果认为某个产品非常优秀，也应该了解它存在哪些问题，不然很有可能导致自己固执己见。&lt;/p&gt;
&lt;h2&gt;另：语文的回旋镖似乎飞回来了&lt;/h2&gt;
&lt;p&gt;写到这里，我突然想起了高中时的语文课。大家都说语文是一门玄学，明明自己认为文章表达的是这个意思，参考答案却往往给出完全不同的解释，写了一大堆还是答不到得分点。&lt;/p&gt;
&lt;p&gt;现在来看，语文的阅读理解其实训练了一项重要能力：从文章中寻找依据，结合上下文分析作者的表达，最后建立有逻辑支撑的答案。我们的理解究竟来自原文，还是来自自己的主观联想？作者真正表达了什么，又有哪些文本证据能够支撑我们的判断？这些问题和今天面对互联网信息时需要进行的思考，其实有不少相似之处。&lt;/p&gt;
&lt;p&gt;高中毕业后，我们很少有机会再安静地阅读并分析一篇完整的文章，抖音、小红书、知乎和 X 倒是刷得越来越多。我们每天接触的信息可能比过去多得多，但真正花在理解、验证和思考上的时间却未必增加。我们越来越习惯于阅读别人提炼好的观点，却很少追问这些观点究竟是怎么得出来的。以前读文章，至少还知道要回到原文寻找依据，但现在面对互联网上的热点事件，我们却常常连原始资料都没有看过，就得出结论甚至与人争论。&lt;/p&gt;
</content:encoded></item><item><title>不要把决策权外包给 AI</title><link>https://mrsibe.top/posts/dont-delegate-decisions-to-ai/</link><guid isPermaLink="true">https://mrsibe.top/posts/dont-delegate-decisions-to-ai/</guid><description>项目确实在前进，但你对它的理解，有没有一起前进？——从一次 Vibe Coding 实训说起，谈谈为什么决策要留给自己、执行才交给 AI，以及怎样把 AI 当作参谋而不是决策者。</description><pubDate>Tue, 08 Sep 2026 07:30:00 GMT</pubDate><content:encoded>&lt;p&gt;上个学期有个学院组织的实训，实训的内容颇为现实：通过Vibe Coding完成一个企业级的IM，还搭建了大模型对话助手和知识库。整个实训的工作量非常大，不过好在有大模型当工具人，几天的时间也可以做一个简单的MVP。技术栈怎么选？问 AI；目录怎么组织？问 AI；下一步应该实现什么？还是问 AI。AI几乎包揽了所有的编码工作，甚至项目答辩PPT也是它做的。项目进展十分顺利，答辩也通过了。但是我回想：我到底学到了什么？似乎什么也没有学到，脑子里空空如也，对项目也没有什么理解，只有一个GitHub仓库证明我们小组曾经做过这个项目。&lt;/p&gt;
&lt;p&gt;这就有一个值得警惕的问题：项目确实在前进，但你对它的理解，有没有一起前进？&lt;/p&gt;
&lt;p&gt;**我们交给 AI 的，有时不只是工作，还有原本可以通过这些工作获得的判断力。**尤其是做一个用于学习、准备长期维护的项目时，决策不是实现之前不得不跨过的障碍，它本身就是理解项目的重要过程。&lt;/p&gt;
&lt;h2&gt;决策会逼你把“知道”变成“理解”&lt;/h2&gt;
&lt;p&gt;做产品或者市场调研的朋友可能听说过 JTBD（Jobs to Be Done），其核心是&lt;strong&gt;弄清楚用户究竟想完成什么事情&lt;/strong&gt;。这种思路放到软件开发里同样成立，技术方案从来不是凭空选出来的，而是在一系列具体约束下做出的取舍：用户要解决什么问题？系统大概有多少用户？并发量会到什么程度？数据规模有多大？&lt;/p&gt;
&lt;p&gt;如果直接问 AI：我的项目应该用什么数据库？它完全可以很快给你一个方案，而且大概率还能附上一套听起来相当充分的理由。你当然也可以继续要求它列出多种方案、分析优缺点，甚至写一份详细的技术选型报告。但这里有一个容易忽视的问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI 可以提供理由，但读过这些理由，不等于形成了自己的判断。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你必须重新回到项目本身，理解它的目标、规模、约束和阶段，然后拿这些条件去衡量不同的方案。所以我越来越觉得，**决策并不是开发之前需要尽快解决掉的麻烦，决策本身就是理解项目的过程。**当我们把这个过程完全交给 AI 时，看起来只是省掉了一次技术选型，实际上也可能顺手省掉了本来最值得学习和理解的那一部分。这也是为什么现在关于 Vibe Coding 的讨论越来越多地转向“可维护性”：真正的问题未必只是 AI 写出的代码质量不好，而是随着开发持续进行，&lt;strong&gt;人和项目之间出现了 context gap——代码越来越多，人对代码为什么变成这样的理解却没有同步增长。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;AI 也不是一个可靠的决策者&lt;/h2&gt;
&lt;p&gt;相信伙伴们Vibe Coding的时候会发现，AI给你一套方案，相当坚定地说“这套方案是最佳方案”。但是你向它提出自己的另外一个方案，它马上改口说：“你的方案确实更好”。当然，如果我们补充了新的约束或证据，改变建议并没有问题。值得警惕的是，没有新的依据，仅仅因为我们表达了倾向，它就调整了结论。你本来是因为不知道怎么判断，才想把决策交给 AI；结果它却把你的态度当成了答案的线索。&lt;/p&gt;
&lt;p&gt;现在的大模型后训练使得模型很容易迎合用户。因此，我们需要检查它的理由是否成立、前提是否符合项目。不能因为它说得自信，就把一项尚未理解的选择当成已经解决的问题。&lt;/p&gt;
&lt;h2&gt;AI 可以成为决策的参谋&lt;/h2&gt;
&lt;p&gt;Linus Torvalds 最近谈到 AI 时说过一句我很认同的话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AI is a tool, just like other tools we use.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;AI其实跟编译器一样，只是个工具。不把决策权交给 AI，并不意味着我们在决策时不能使用 AI。恰恰相反，搜集资料、补充知识、比较方案、验证想法，这些工作都可以借助它完成。重要的是，在这个过程中，我们有没有形成自己的判断。&lt;/p&gt;
&lt;p&gt;还是拿前面的数据库选型来说。与其直接问“我的项目应该用什么数据库”，不如先写下自己对项目的理解：我要解决什么问题？哪些能力是现在就需要的？哪些只是对未来的想象？对目前这个阶段来说，我更在意开发速度、部署成本，还是某一种特定能力？这些认识未必完整，也未必正确，但至少是一个可以讨论和修正的起点。&lt;/p&gt;
&lt;p&gt;有了这些条件，再让 AI 给出候选方案，分析各自的代价，指出我们遗漏的约束。它甚至可以帮助我们建立比较方案的维度，但这些维度是否符合项目的目标，仍然需要我们理解和确认。不能因为一份选型报告写得完整，就默认里面每一个理由对自己的项目都同样重要。&lt;/p&gt;
&lt;p&gt;比如，AI 建议给项目引入缓存。我们可以继续追问：它具体要解决哪个问题？这个问题已经出现了，还是仅仅可能出现？不引入缓存，会付出什么代价？引入之后，又有哪些额外的复杂性需要处理？如果当前目标只是完成一个功能有限的演示版本，这些代价值得现在承担吗？&lt;/p&gt;
&lt;p&gt;这时，AI 的作用就不只是给出一个“用”或者“不用”的答案，而是帮助我们把一个模糊的问题拆开，让原本没有注意到的取舍变得清楚，决策权仍在自己手上，&lt;/p&gt;
&lt;h2&gt;决策留给自己，执行交给 AI&lt;/h2&gt;
&lt;p&gt;当然，保留决策权，并不意味着每一个变量怎么命名、每一个函数怎么拆分，最好是区分哪些判断值得自己深入参与，哪些细节可以在明确的边界内交给 AI。项目要解决什么问题，哪些需求暂时不做，这些事情应该由自己掌握。至于在已经确定的接口和约束下补充实现、编写重复代码、整理文档，则可以给予 AI 更多自主空间。&lt;/p&gt;
&lt;p&gt;回头看那次实训，我真正遗憾的，并不是让 AI 写了太多代码，而是没有借着项目里的那些问题，认真做过几次选择。为什么采用这套技术栈？为什么需要这个组件？哪些功能其实可以不做？如果重新来一次，我仍然会使用 AI，但我更愿意少完成一些功能，换来对这些问题更清楚的回答。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我希望一个项目最后留给我的，不只是一个 GitHub 仓库，还有下一次面对类似问题时，能够自己做出判断的能力。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>用 Go 实现 Redis（二）：用 map 实现内存数据库与命令分发</title><link>https://mrsibe.top/posts/godis/2-in-memory-db-and-command-dispatch/</link><guid isPermaLink="true">https://mrsibe.top/posts/godis/2-in-memory-db-and-command-dispatch/</guid><description>本文是「用 Go 实现 Redis」系列的第二篇。我们先用 map 与读写锁实现一个简单的内存数据库，再用 Object 统一不同类型的数据，最后通过命令注册表，把 RESP 请求分发到对应的命令处理函数。</description><pubDate>Tue, 01 Sep 2026 02:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上一篇文章介绍了 Godis 中 RESP2 协议的实现。通过 RESP Parser，Godis 已经能够把客户端发送的字节流解析成 &lt;code&gt;resp.Value&lt;/code&gt;；通过 RESP Writer，也能够把执行结果重新编码成 RESP 回复。&lt;/p&gt;
&lt;p&gt;不过，协议层只解决了客户端与服务端之间如何交换数据的问题。当 Parser 解析出下面这条命令后：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET name MrSibe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Godis 还需要回答两个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt; 和 &lt;code&gt;MrSibe&lt;/code&gt; 最终应该保存在哪里？&lt;/li&gt;
&lt;li&gt;Godis 如何知道应该执行 &lt;code&gt;SET&lt;/code&gt;，而不是 &lt;code&gt;GET&lt;/code&gt; 或其他命令？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这篇文章将实现 Godis 的内存数据库，并通过一个简单的命令注册表，把 RESP 协议层、命令层和数据库层连接起来。&lt;/p&gt;
&lt;p&gt;Github仓库：&lt;a href=&quot;https://github.com/MrSibe/godis&quot;&gt;MrSibe/godis: A Redis-compatible in-memory key-value store written in Go.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;1. 从一个简单的 map 开始&lt;/h2&gt;
&lt;p&gt;Godis 是一个内存键值数据库，因此最直接的存储方式就是使用 Go 内置的 &lt;code&gt;map&lt;/code&gt;。如果当前只考虑 String 类型，可以先写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type DB struct {
    data map[string]string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如 &lt;code&gt;db.data[&quot;name&quot;] = &quot;MrSibe&quot;&lt;/code&gt;。在这个基础上，实现 &lt;code&gt;SET&lt;/code&gt;、&lt;code&gt;GET&lt;/code&gt; 和 &lt;code&gt;DEL&lt;/code&gt; 并不复杂：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (db *DB) Set(key, value string) {
    db.data[key] = value
}

func (db *DB) Get(key string) (string, bool) {
    value, ok := db.data[key]
    return value, ok
}

func (db *DB) Del(key string) bool {
    if _, ok := db.data[key]; !ok {
        return false
    }

    delete(db.data, key)
    return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过，&lt;code&gt;map[string]string&lt;/code&gt; 只适合最初的 String 类型。随着项目继续开发，Godis 还需要支持 List、Hash、Set 和 ZSet 等不同的数据类型，因此需要一个更通用的数据表示方式。&lt;/p&gt;
&lt;h2&gt;2. 用 Object 统一表示不同类型的数据&lt;/h2&gt;
&lt;p&gt;在 Godis 中，我定义了一个 &lt;code&gt;ObjectType&lt;/code&gt; 表示对象类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package database

type ObjectType uint8

const (
    StringType ObjectType = iota
    ListType
    HashType
    SetType
    ZSetType
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后定义一个 &lt;code&gt;Object&lt;/code&gt;，同时保存对象类型和实际数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Object struct {
    Type  ObjectType
    Value any
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，数据库就不再使用&lt;code&gt;map[string]string&lt;/code&gt;，而是使用：&lt;code&gt;map[string]*Object&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;3. 封装数据库结构与操作&lt;/h2&gt;
&lt;p&gt;最终，Godis 的数据库结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type DB struct {
    mu   sync.RWMutex
    data map[string]*Object
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Godis Server 可以接受多个TCP连接，但 Go 内置 map 并不保证无保护的并发读写安全。因此，Godis 使用 &lt;code&gt;sync.RWMutex&lt;/code&gt; 保护底层 map。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RWMutex&lt;/code&gt; 是一种读写锁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以同时被多个读操作持有；&lt;/li&gt;
&lt;li&gt;写操作需要独占整个锁；&lt;/li&gt;
&lt;li&gt;有写操作时，其他读写操作都需要等待。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;func (db *DB) Set(key string, value *Object) {
    db.mu.Lock()
    defer db.mu.Unlock()

    db.data[key] = value
}

func (db *DB) Get(key string) (*Object, bool) {
    db.mu.RLock()
    defer db.mu.RUnlock()

    value, ok := db.data[key]
    return value, ok
}

func (db *DB) Del(key string) bool {
    db.mu.Lock()
    defer db.mu.Unlock()

    if _, ok := db.data[key]; !ok {
        return false
    }

    delete(db.data, key)
    return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 为什么没有使用 sync.Map&lt;/h2&gt;
&lt;p&gt;Go 标准库还提供了并发安全的 &lt;code&gt;sync.Map&lt;/code&gt;，那么为什么 Godis 仍然选择普通 map 加 &lt;code&gt;RWMutex&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;一个原因是 &lt;code&gt;sync.Map&lt;/code&gt; 是面向特定访问模式设计的，而不是普通 map 的通用替代品。&lt;a href=&quot;https://pkg.go.dev/sync&quot;&gt;Go 官方文档&lt;/a&gt;也建议，大多数代码使用普通 map 配合单独的锁，这样能够获得更好的类型安全，也更容易维护与 map 内容相关的其他约束。等到将来通过 Benchmark 确定锁确实成为性能瓶颈后，再考虑分片锁或者其他并发方案会更加合理。&lt;/p&gt;
&lt;h2&gt;5. 从 RESP 到数据库操作&lt;/h2&gt;
&lt;p&gt;到这里，Godis 已经有了两个独立的模块：RESP 负责字节流与 &lt;code&gt;resp.Value&lt;/code&gt; 之间的转换，数据库层负责 key 与对象的存储。但二者还没有被连接起来。&lt;/p&gt;
&lt;p&gt;以 &lt;code&gt;SET name MrSibe&lt;/code&gt; 为例，RESP Parser 会把它解析成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resp.Value{
    Type: resp.Array,
    Array: []resp.Value{
        {Type: resp.BulkString, String: &quot;SET&quot;},
        {Type: resp.BulkString, String: &quot;name&quot;},
        {Type: resp.BulkString, String: &quot;MrSibe&quot;},
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要让这条命令真正写入数据库，中间还需要一个模块完成下面的转换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[&quot;SET&quot;, &quot;name&quot;, &quot;MrSibe&quot;]
            ↓
找到 SET 对应的执行函数
            ↓
调用 db.Set(&quot;name&quot;, &quot;MrSibe&quot;)
            ↓
返回 +OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最直接的做法是在一个函数里用巨大的 &lt;code&gt;switch&lt;/code&gt; 分发命令，但命令越来越多时，&lt;code&gt;switch&lt;/code&gt; 会不断膨胀。因此 Godis 使用命令注册表：用 map 把命令名映射到执行函数，新增命令时只需要注册一条映射。&lt;/p&gt;
&lt;h2&gt;6. 定义统一的 Handler 与命令注册表&lt;/h2&gt;
&lt;p&gt;为了让所有命令都能放进同一个 map，先定义统一的函数签名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Handler func(
    db *database.DB,
    args []resp.Value,
) resp.Value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;db&lt;/code&gt; 是所有命令共享的数据库实例；&lt;code&gt;args&lt;/code&gt; 是除命令名以外的参数，命令名已经由分发器取出。例如 &lt;code&gt;SET name MrSibe&lt;/code&gt; 对应的 &lt;code&gt;args&lt;/code&gt; 是 &lt;code&gt;[&quot;name&quot;, &quot;MrSibe&quot;]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;命令层不直接向 TCP 连接写入字节，而是返回一个 &lt;code&gt;resp.Value&lt;/code&gt;，交给 Writer 序列化。即使命令不需要访问数据库，也保持相同的签名，例如 &lt;code&gt;PING&lt;/code&gt; 用 &lt;code&gt;_&lt;/code&gt; 忽略 &lt;code&gt;db&lt;/code&gt; 参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func ping(_ *database.DB, args []resp.Value) resp.Value {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，命令注册表就是一张普通的映射表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var handlers = map[string]Handler{
    &quot;PING&quot;:   ping,
    &quot;ECHO&quot;:   echo,
    &quot;SET&quot;:    set,
    &quot;GET&quot;:    get,
    &quot;DEL&quot;:    del,
    &quot;EXISTS&quot;: exists,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加新命令只需要两步：实现一个 Handler，然后在 &lt;code&gt;handlers&lt;/code&gt; 中注册，分发器完全不需要了解命令内部的实现。Godis 当前注册了 &lt;code&gt;PING&lt;/code&gt;、&lt;code&gt;ECHO&lt;/code&gt;、&lt;code&gt;SET&lt;/code&gt;、&lt;code&gt;GET&lt;/code&gt;、&lt;code&gt;DEL&lt;/code&gt; 和 &lt;code&gt;EXISTS&lt;/code&gt; 六个命令。&lt;/p&gt;
&lt;h2&gt;7. 实现命令分发&lt;/h2&gt;
&lt;p&gt;分发由 &lt;code&gt;Execute&lt;/code&gt; 完成，它校验请求格式，取出并规范化命令名，最后从注册表中找到 Handler 并调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Execute(db *database.DB, req resp.Value) resp.Value {
    if req.Type != resp.Array || len(req.Array) == 0 {
        return resp.Value{
            Type:   resp.Error,
            String: &quot;ERR invalid command&quot;,
        }
    }

    commandName := strings.ToUpper(req.Array[0].String)

    handler, ok := handlers[commandName]
    if !ok {
        return resp.Value{
            Type:   resp.Error,
            String: &quot;ERR unknown command &apos;&quot; + req.Array[0].String + &quot;&apos;&quot;,
        }
    }

    return handler(db, req.Array[1:])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它依次完成四件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;校验请求格式&lt;/strong&gt;：一条标准命令请求必须是非空 Array。既不是 Array、或者 Array 为空都会返回 &lt;code&gt;ERR invalid command&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;取出命令名&lt;/strong&gt;：Array 的第一个元素；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规范化并查找&lt;/strong&gt;：Redis 命令不区分大小写，所以先用 &lt;code&gt;strings.ToUpper&lt;/code&gt; 转成大写再查表，&lt;code&gt;GET&lt;/code&gt;、&lt;code&gt;get&lt;/code&gt;、&lt;code&gt;Get&lt;/code&gt; 都能命中同一个 Handler；查不到则返回 &lt;code&gt;ERR unknown command &apos;xxx&apos;&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行&lt;/strong&gt;：把 &lt;code&gt;req.Array[1:]&lt;/code&gt;——去掉命令名后的参数——连同数据库实例一起传给 Handler。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;8. 实现 SET 与 GET&lt;/h2&gt;
&lt;h3&gt;8.1 SET&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;SET&lt;/code&gt; 需要两个参数。参数数量不对时直接返回错误；正确时把值包装成 &lt;code&gt;StringType&lt;/code&gt; 的 Object 写入数据库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func set(db *database.DB, args []resp.Value) resp.Value {
    if len(args) != 2 {
        return resp.Value{
            Type:   resp.Error,
            String: &quot;ERR wrong number of arguments for &apos;set&apos; command&quot;,
        }
    }

    db.Set(args[0].String, &amp;amp;database.Object{
        Type:  database.StringType,
        Value: args[1].String,
    })

    return resp.Value{
        Type:   resp.SimpleString,
        String: &quot;OK&quot;,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如 &lt;code&gt;SET name&lt;/code&gt; 或 &lt;code&gt;SET name MrSibe extra&lt;/code&gt; 都会因为参数数量不等于 2 而报错。执行成功返回的 &lt;code&gt;SimpleString(&quot;OK&quot;)&lt;/code&gt; 会被 Writer 编码为 &lt;code&gt;+OK\r\n&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;8.2 GET&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;GET&lt;/code&gt; 只接收一个 key，读取后要区分三种情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func get(db *database.DB, args []resp.Value) resp.Value {
    if len(args) != 1 {
        return resp.Value{
            Type:   resp.Error,
            String: &quot;ERR wrong number of arguments for &apos;get&apos; command&quot;,
        }
    }

    obj, ok := db.Get(args[0].String)
    if !ok {
        return resp.Value{
            Type: resp.BulkString,
            Null: true,
        }
    }

    if obj.Type != database.StringType {
        return resp.Value{
            Type:   resp.Error,
            String: &quot;WRONGTYPE Operation against a key holding the wrong kind of value&quot;,
        }
    }

    return resp.Value{
        Type:   resp.BulkString,
        String: obj.Value.(string),
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;key 不存在&lt;/strong&gt;：返回 Null Bulk String，被序列化为 &lt;code&gt;$-1\r\n&lt;/code&gt;。注意不能返回空字符串，因为空字符串本身也可能是合法值；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型不匹配&lt;/strong&gt;：key 存在但不是 String 时返回 &lt;code&gt;WRONGTYPE&lt;/code&gt; 错误。类型检查必须先于类型断言 &lt;code&gt;obj.Value.(string)&lt;/code&gt;，否则断言会 panic；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;正常情况&lt;/strong&gt;：把 String 内容包装成 Bulk String 返回，例如 &lt;code&gt;$6\r\nMrSibe\r\n&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;9. 实现 DEL 与 EXISTS&lt;/h2&gt;
&lt;h3&gt;9.1 DEL&lt;/h3&gt;
&lt;p&gt;当前 &lt;code&gt;DEL&lt;/code&gt; 只支持一个 key，删除成功与 key 不存在时分别返回 &lt;code&gt;:1&lt;/code&gt; 与 &lt;code&gt;:0&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func del(db *database.DB, args []resp.Value) resp.Value {
    if len(args) != 1 {
        return resp.Value{
            Type:   resp.Error,
            String: &quot;ERR wrong number of arguments for &apos;del&apos; command&quot;,
        }
    }

    if db.Del(args[0].String) {
        return resp.Value{
            Type:    resp.Integer,
            Integer: 1,
        }
    }

    return resp.Value{
        Type:    resp.Integer,
        Integer: 0,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9.2 EXISTS&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;EXISTS&lt;/code&gt; 接收一个或多个 key，统计其中存在的个数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func exists(db *database.DB, args []resp.Value) resp.Value {
    if len(args) == 0 {
        return resp.Value{
            Type:   resp.Error,
            String: &quot;ERR wrong number of arguments for &apos;exists&apos; command&quot;,
        }
    }

    count := 0
    for _, arg := range args {
        if _, ok := db.Get(arg.String); ok {
            count++
        }
    }

    return resp.Value{
        Type:    resp.Integer,
        Integer: count,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如数据库中存在 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 时，&lt;code&gt;EXISTS a b c&lt;/code&gt; 返回 &lt;code&gt;:2&lt;/code&gt;——三个 key 里只有两个存在。&lt;/p&gt;
&lt;h2&gt;10. 一条 SET 命令的完整执行过程&lt;/h2&gt;
&lt;p&gt;现在把两篇文章中的模块连接起来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    Client[Redis Client]
    Parser[RESP Parser]
    Request[RESP Value]
    Execute[Command Execute]
    Registry[Handler Registry]
    Handler[SET Handler]
    DB[(In-memory DB)]
    Reply[RESP Value]
    Writer[RESP Writer]

    Client --&amp;gt;|RESP 字节流| Parser
    Parser --&amp;gt; Request
    Request --&amp;gt; Execute
    Execute --&amp;gt; Registry
    Registry --&amp;gt; Handler
    Handler --&amp;gt; DB
    Handler --&amp;gt; Reply
    Reply --&amp;gt; Writer
    Writer --&amp;gt;|RESP 字节流| Client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端发送的 &lt;code&gt;SET name MrSibe&lt;/code&gt; 在网络上被编码成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$6\r\nMrSibe\r\n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后依次发生：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Parser 把它解析成 Array 形式的 &lt;code&gt;resp.Value&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Execute&lt;/code&gt; 校验请求、取出命令名 &lt;code&gt;SET&lt;/code&gt;，并从注册表中找到 &lt;code&gt;set&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set&lt;/code&gt; 检查参数数量，构造一个 &lt;code&gt;StringType&lt;/code&gt; 的 Object；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;db.Set&lt;/code&gt; 在写锁保护下把对象写入 map；&lt;/li&gt;
&lt;li&gt;返回 &lt;code&gt;SimpleString(&quot;OK&quot;)&lt;/code&gt;，Writer 编码为 &lt;code&gt;+OK\r\n&lt;/code&gt; 写回客户端。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;服务端每个连接的处理循环中，核心调用只有一行 &lt;code&gt;command.Execute(s.db, value)&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for {
    value, err := parser.Parse()
    if err != nil {
        return
    }

    reply := command.Execute(s.db, value)

    if err := writer.Write(reply); err != nil {
        return
    }

    if err := writer.Flush(); err != nil {
        return
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当前 Godis 的 Server 为所有连接共享同一个数据库实例，并在每个连接的处理循环中依次完成解析、执行、序列化和刷新。&lt;a href=&quot;https://raw.githubusercontent.com/MrSibe/godis/main/internal/server/server.go&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;12. 当前实现还存在哪些不足&lt;/h2&gt;
&lt;p&gt;目前的实现已经能支撑基础命令，但仍然是早期版本，存在几个明显的局限。&lt;/p&gt;
&lt;h3&gt;12.1 map 并发安全不等于命令原子性&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;DB.Get&lt;/code&gt;、&lt;code&gt;DB.Set&lt;/code&gt; 和 &lt;code&gt;DB.Del&lt;/code&gt; 的锁只覆盖单次方法调用。例如 &lt;code&gt;EXISTS&lt;/code&gt; 循环调用多次 &lt;code&gt;Get&lt;/code&gt;，每次 &lt;code&gt;Get&lt;/code&gt; 返回都会释放读锁，两次检查之间其他客户端可能修改数据库。也就是说“每一次 map 访问并发安全”并不等于“整条命令原子”。以后实现多 key 命令和事务时，需要让锁覆盖整条命令，或引入更明确的读写事务接口。&lt;/p&gt;
&lt;h3&gt;12.2 Object 暴露了可变数据&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Get&lt;/code&gt; 直接返回 &lt;code&gt;*Object&lt;/code&gt;，而 &lt;code&gt;Object.Value&lt;/code&gt; 是公开的 &lt;code&gt;any&lt;/code&gt;。String 本身不可变，暂时没有风险；将来加入 List、Hash 这类可变结构后，命令层可能在数据库锁之外修改它们。届时可以让字段私有、为不同类型提供构造函数、把复合对象的修改收敛到数据库锁内，或引入 &lt;code&gt;View&lt;/code&gt;、&lt;code&gt;Update&lt;/code&gt; 之类的访问接口。&lt;/p&gt;
&lt;h3&gt;12.3 注册表只有 Handler&lt;/h3&gt;
&lt;p&gt;参数数量检查由每个 Handler 自己重复编写。以后可以把注册信息扩展成带元数据的结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Command struct {
    Name    string
    Arity   int
    Flags   CommandFlags
    Handler Handler
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分发器就可以统一检查参数，也能为后续的锁、AOF 和权限系统提供命令元数据。&lt;/p&gt;
&lt;h3&gt;12.4 请求参数缺少严格的类型检查&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Execute&lt;/code&gt; 默认请求是 Array 且元素都是 Bulk String，直接读取 &lt;code&gt;req.Array[0].String&lt;/code&gt;。标准客户端确实符合这个结构，但服务端仍应防御格式错误的请求。后续可以把请求先转换成明确的结构，例如 &lt;code&gt;Request{Command string; Args [][]byte}&lt;/code&gt;，在转换时统一校验请求类型与参数类型。&lt;/p&gt;
&lt;h3&gt;12.5 命令兼容性有限&lt;/h3&gt;
&lt;p&gt;当前只实现了最基础的形式：&lt;code&gt;DEL&lt;/code&gt; 只支持一个 key，&lt;code&gt;SET&lt;/code&gt; 也还没有过期时间和条件写入等选项，与真实 Redis 的行为仍有差距，需要通过兼容性测试逐步补齐。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;到这里，Godis 已经拥有了一条完整的基础命令执行链路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RESP Parser
    ↓
Command Dispatcher
    ↓
Command Handler
    ↓
In-memory Database
    ↓
RESP Writer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数据库层用 &lt;code&gt;map[string]*Object&lt;/code&gt; 保存 key 与对象之间的映射，用 &lt;code&gt;sync.RWMutex&lt;/code&gt; 保护多个客户端的并发访问；命令层用 &lt;code&gt;map[string]Handler&lt;/code&gt; 建立命令名与执行函数之间的映射。分发器只负责识别命令，参数检查和具体的数据库操作由对应的 Handler 完成。&lt;/p&gt;
&lt;p&gt;这一部分的代码量并不大，但实现之后，我对几个问题有了更具体的认识：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个普通 map 如何被封装成数据库接口；&lt;/li&gt;
&lt;li&gt;key 不存在和空字符串为什么必须区分；&lt;/li&gt;
&lt;li&gt;多个连接共享数据库时为什么需要进行并发控制；&lt;/li&gt;
&lt;li&gt;命令逻辑为什么不应该堆在一个巨大的 &lt;code&gt;switch&lt;/code&gt; 中；&lt;/li&gt;
&lt;li&gt;RESP Value 如何经过命令分发，最终变成一次数据库操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目前 Godis 已经可以通过 &lt;code&gt;redis-cli&lt;/code&gt; 执行 &lt;code&gt;PING&lt;/code&gt;、&lt;code&gt;ECHO&lt;/code&gt;、&lt;code&gt;SET&lt;/code&gt;、&lt;code&gt;GET&lt;/code&gt;、&lt;code&gt;DEL&lt;/code&gt; 和 &lt;code&gt;EXISTS&lt;/code&gt;，但它仍然只是一个不会保存过期时间、重启后会丢失全部数据的内存数据库。&lt;/p&gt;
</content:encoded></item><item><title>用 Go 实现 Redis（一）：RESP2 协议的实现</title><link>https://mrsibe.top/posts/godis/1-resp-protocol/</link><guid isPermaLink="true">https://mrsibe.top/posts/godis/1-resp-protocol/</guid><description>本文是「用 Go 实现 Redis」系列的第一篇，主要讨论 RESP2 协议（Redis Serialization Protocol）。我们先从一条命令的传输过程讲起，再介绍 RESP2 的五种基础类型，最后给出 Parser 解析器和 Writer 序列化器的实现。</description><pubDate>Mon, 31 Aug 2026 02:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近我在阅读《Redis设计与实现》这本业界相当有名的书。这本书从数据结构、对象系统、数据库、持久化等角度介绍了 Redis 的内部实现。不过说实话光是看一本书并不会对Redis有太深的印象，边看书边去阅读Redis的源代码才对Redis有更深的印象。为了加深对 Redis 内部设计的理解，我决定使用 Go 实现一个简化版 Redis，并把它命名为 &lt;strong&gt;Godis&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Github仓库：&lt;a href=&quot;https://github.com/MrSibe/godis&quot;&gt;MrSibe/godis: A Redis-compatible in-memory key-value store written in Go.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这篇文章先不讨论数据库和命令实现，而是从整个项目最底层的通信协议开始：&lt;strong&gt;RESP2&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;1. Redis 客户端与服务端如何通信&lt;/h2&gt;
&lt;p&gt;当我们在 &lt;code&gt;redis-cli&lt;/code&gt; 中输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET name MrSibe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端并不是简单地把这一行文本原样发送给 Redis。&lt;/p&gt;
&lt;p&gt;Redis 客户端和服务端之间使用一种名为 &lt;strong&gt;RESP&lt;/strong&gt;（Redis Serialization Protocol）的序列化协议。在 RESP 中，每个值的第一个字节表示这个值的类型，各个协议部分通常使用 &lt;code&gt;\r\n&lt;/code&gt;，也就是 CRLF 作为结束标记。客户端发送命令时，通常会把命令表示成一个由 Bulk String 组成的 Array。&lt;/p&gt;
&lt;p&gt;前面的命令会被编码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*3\r\n
$3\r\n
SET\r\n
$4\r\n
name\r\n
$6\r\n
MrSibe\r\n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;*3&lt;/code&gt;表示这是一个包含三个元素的数组。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;$3&lt;/code&gt;表示这是长度为 3 字节的 Bulk String，内容为&lt;code&gt;SET&lt;/code&gt;。后面的元素同理。&lt;/p&gt;
&lt;p&gt;命令执行成功后，服务端返回：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+OK\r\n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里开头的 &lt;code&gt;+&lt;/code&gt; 表示返回值是一个 Simple String。&lt;/p&gt;
&lt;h2&gt;2. RESP2 中的五种基础类型&lt;/h2&gt;
&lt;p&gt;Godis 当前主要实现的是 RESP2。RESP2 中常见的五种类型如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;前缀&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;th&gt;常见用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple String&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+OK\r\n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回简单成功信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-ERR ...\r\n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回错误&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:1\r\n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulk String&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$5\r\nhello\r\n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回普通字符串或二进制数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Array&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*2\r\n...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;表示多个 RESP 值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;需要注意的是，当 &lt;code&gt;GET&lt;/code&gt; 查询的 key 不存在时，不能返回空字符串，因为空字符串本身也可能是一个合法的值。RESP2 使用 Null Bulk String &lt;code&gt;$-1&lt;/code&gt;表示不存在，Null Array 则表示为&lt;code&gt;*-1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我在 &lt;code&gt;Godis&lt;/code&gt;中定义了 &lt;code&gt;Type&lt;/code&gt;和&lt;code&gt;Value&lt;/code&gt;来统一表示不同的 RESP 类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package resp

type Type byte

const (
    SimpleString Type = &apos;+&apos;
    Error Type = &apos;-&apos;
    Integer Type = &apos;:&apos;
    BulkString Type = &apos;$&apos;
    Array Type = &apos;*&apos;
)

type Value struct {
    Type Type
    String  string
    Integer int
    Array   []Value
    Null bool
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Type&lt;/code&gt; 决定当前应该读取&lt;code&gt;Value&lt;/code&gt;的哪个字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simple String、Error 和 Bulk String 使用 &lt;code&gt;String&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Integer 使用 &lt;code&gt;Integer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Array 使用 &lt;code&gt;Array&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Null&lt;/code&gt; 用于区分普通值和 RESP 中的 Null 值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// +OK\r\n
Value{
    Type:   SimpleString,
    String: &quot;OK&quot;,
}
// :1\r\n
Value{
    Type: Integer,
    Integer: 1,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 实现 RESP Parser&lt;/h2&gt;
&lt;p&gt;RESP 解析器的任务，是把网络连接中的字节转换成前面的 &lt;code&gt;Value&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Godis 中的 &lt;code&gt;Parser&lt;/code&gt; 包装了一个 &lt;code&gt;bufio.Reader&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Parser struct {
    reader *bufio.Reader
}

func NewParser(rd io.Reader) *Parser {
    return &amp;amp;Parser{
        reader: bufio.NewReader(rd),
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里接收的是 &lt;code&gt;io.Reader&lt;/code&gt;，而不是直接依赖 &lt;code&gt;net.Conn&lt;/code&gt;。这样设计后，生产环境可以传入 TCP 连接，测试时则可以直接传入字符串，这使解析器和具体的网络连接实现解耦，也让单元测试更加方便。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Parse&lt;/code&gt; 首先读取一个字节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (p *Parser) Parse() (Value, error) {
    prefix, err := p.reader.ReadByte()
    if err != nil {
        return Value{}, err
    }

    switch Type(prefix) {
    case Array:
        return p.parseArray()
    case BulkString:
        return p.parseBulkString()
    default:
        return Value{}, fmt.Errorf(
            &quot;unsupported RESP type: %q&quot;,
            prefix,
        )
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.1. 解析 Bulk String&lt;/h3&gt;
&lt;p&gt;Bulk String 的格式是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$&amp;lt;字符串长度&amp;gt;\r\n
&amp;lt;字符串内容&amp;gt;\r\n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如&lt;code&gt;$5\r\nhello\r\n&lt;/code&gt;，解析时，需要先读取长度&lt;code&gt;5&lt;/code&gt;，然后准确读取 5 字节的正文，以及正文后面的两个 CRLF 字节后终止。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里有个有意思的设计：为什么必须显示声明字符串的长度？&lt;/p&gt;
&lt;p&gt;这是因为 Bulk String 的内容本身可能包含 &lt;code&gt;\r\n&lt;/code&gt;，所以不能简单地用“读到下一行”的方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Godis 中的核心逻辑可以简化为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (p *Parser) parseBulkString() (Value, error) {
    line, err := p.readLine()
    if err != nil {
        return Value{}, err
    }

    length, err := strconv.Atoi(line)
    if err != nil {
        return Value{}, err
    }

    if length &amp;lt; 0 {
        return Value{
            Type: BulkString,
            Null: true,
        }, nil
    }

    buf := make([]byte, length+2)
    if _, err := io.ReadFull(p.reader, buf); err != nil {
        return Value{}, err
    }

    return Value{
        Type:   BulkString,
        String: string(buf[:length]),
    }, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Godis 使用 &lt;code&gt;io.ReadFull&lt;/code&gt; 读取指定长度的数据。只要正文还没有读取完整，它就会继续从底层 Reader 中获取数据。这种方式也更适合处理 TCP 数据没有一次性全部到达的情况。&lt;/p&gt;
&lt;h3&gt;3.2. 递归解析 Array&lt;/h3&gt;
&lt;p&gt;Array 的格式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*&amp;lt;元素数量&amp;gt;\r\n
&amp;lt;元素一&amp;gt;
&amp;lt;元素二&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*2\r\n$3\r\nGET\r\n$4\r\nname\r\n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解析器首先读出元素数量 &lt;code&gt;2&lt;/code&gt;，然后调用两次 &lt;code&gt;Parse&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (p *Parser) parseArray() (Value, error) {
    line, err := p.readLine()
    if err != nil {
        return Value{}, err
    }

    length, err := strconv.Atoi(line)
    if err != nil {
        return Value{}, err
    }

    if length &amp;lt; 0 {
        return Value{
            Type: Array,
            Null: true,
        }, nil
    }

    values := make([]Value, 0, length)

    for range length {
        value, err := p.Parse()
        if err != nil {
            return Value{}, err
        }

        values = append(values, value)
    }

    return Value{
        Type:  Array,
        Array: values,
    }, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 实现 RESP Writer&lt;/h2&gt;
&lt;p&gt;Parser 负责把字节流转换成&lt;code&gt;Value&lt;/code&gt;，而 Writer 完全相反：把&lt;code&gt;Value&lt;/code&gt;转换成字节流。&lt;/p&gt;
&lt;p&gt;Godis 的 Writer 同样包装了一个缓冲区：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Writer struct {
    writer *bufio.Writer
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;Write&lt;/code&gt; 方法中，根据 &lt;code&gt;Value.Type&lt;/code&gt; 选择相应的序列化方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;switch value.Type {
case SimpleString:
    // +OK\r\n
case Error:
    // -ERR ...\r\n
case Integer:
    // :1\r\n
case BulkString:
    // $5\r\nhello\r\n
case Array:
    // *2\r\n...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于底层使用了 &lt;code&gt;bufio.Writer&lt;/code&gt;，调用 &lt;code&gt;Write&lt;/code&gt; 后，数据不一定已经写入 TCP 连接。因此，每次回复结束后还需要调用&lt;code&gt;writer.Flush()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Godis 当前的 Writer 可以序列化 Simple String、Error、Integer、Bulk String 和 Array，并能够根据类型输出 &lt;code&gt;$-1&lt;/code&gt; 或 &lt;code&gt;*-1&lt;/code&gt; 形式的 Null。&lt;/p&gt;
&lt;h2&gt;5. 一条命令在 Godis 中的完整流程&lt;/h2&gt;
&lt;p&gt;将 Parser、命令执行器和 Writer 组合起来，一条命令的处理过程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    Client[redis-cli / Redis Client]
    TCP[TCP Connection]
    Parser[RESP Parser]
    Command[Command Executor]
    DB[In-memory Database]
    Writer[RESP Writer]

    Client --&amp;gt;|RESP Request| TCP
    TCP --&amp;gt; Parser
    Parser --&amp;gt;|Value| Command
    Command --&amp;gt; DB
    DB --&amp;gt; Command
    Command --&amp;gt;|Reply Value| Writer
    Writer --&amp;gt;|RESP Reply| TCP
    TCP --&amp;gt; Client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Godis 的连接处理函数中，这个过程对应下面的循环：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for {
    value, err := parser.Parse()
    if err != nil {
        return
    }

    reply := command.Execute(db, value)

    if err := writer.Write(reply); err != nil {
        return
    }

    if err := writer.Flush(); err != nil {
        return
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每建立一个客户端连接，Godis 都会为该连接创建一个 Parser 和 Writer。之后不断读取一个完整 RESP 值、执行命令、生成回复并写回连接。&lt;/p&gt;
&lt;h2&gt;6. 当前实现还存在哪些不足&lt;/h2&gt;
&lt;p&gt;现在的 Godis 实现了满足基础命令通信需要的 RESP2 子集，而不是完整实现了所有 Redis 协议能力。比如说，Parser 只支持作为命令请求使用的 Array 和 Bulk String，没有实现 RESP3，也没有实现 Redis 的 Inline Command 格式。&lt;/p&gt;
&lt;p&gt;后续我会继续完善协议解析，并逐步实现命令分发、内存数据库、过期机制和持久化。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;实现 RESP2 之后，我对 Redis 客户端和服务端之间的通信过程有了更加具体的认识。&lt;/p&gt;
&lt;p&gt;原本看起来只是一条简单的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET name MrSibe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;真正进入网络后，会变成带有类型标记和长度信息的字节流。服务端需要解析 Array 和 Bulk String，将其转换为命令与参数；命令执行完成后，又要把结果序列化成不同类型的 RESP 回复。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/reference/protocol-spec/&quot;&gt;Redis Protocol specification&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/MrSibe/godis&quot;&gt;MrSibe/godis: A Redis-compatible in-memory key-value store written in Go.&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>从 OpenClaw 到 Hermes Agent：我对记忆、Skill 和 Agent 架构的理解</title><link>https://mrsibe.top/posts/agent/openclaw/</link><guid isPermaLink="true">https://mrsibe.top/posts/agent/openclaw/</guid><description>用了 OpenClaw 和 Hermes Agent 一段时间后，我重新整理了记忆、Skill、工具调用与执行环境之间的关系，并结合两者的设计差异，谈谈我对 Agent 模块整合方式和未来演进路线的一些理解。</description><pubDate>Thu, 16 Apr 2026 22:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;最后核对日期：2026-08-29。本文包含我的使用感受和架构判断；项目已经实现的能力以各自官方文档为准。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最近 OpenClaw、Hermes Agent 火的一塌糊涂，我也用了几个月，一直想针对这方面写一些自己的见解。&lt;/p&gt;
&lt;h2&gt;OpenClaw 为什么火了？&lt;/h2&gt;
&lt;p&gt;在 OpenClaw 出圈之前，程序员接触最多的 Agent 可能就是 Claude Code 了。大家看到 OpenClaw 可能会想：都是通过聊天来让 Agent 做事，凭什么 OpenClaw 可以这么火？&lt;/p&gt;
&lt;p&gt;我认为理由有两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenClaw 真正把“高深莫测”的 Agent 放在了&lt;strong&gt;普通大众&lt;/strong&gt;面前。&lt;/li&gt;
&lt;li&gt;OpenClaw 把 Agent &lt;strong&gt;模块标准化&lt;/strong&gt;了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;在哪和 Agent 聊天很重要&lt;/h3&gt;
&lt;p&gt;OpenClaw 火的重要原因就是它可以接入到各种 IM 聊天工具里，例如 QQ、微信、Telegram、Discord 等等。&lt;/p&gt;
&lt;p&gt;想一想，假如是 Claude Code，我们需要打开终端，安装 Claude Code CLI。终端对于不怎么接触计算机的小伙伴本身就是一种很有负担的东西：UI、UX 并不友好。即使是配合 VS Code 使用，计算机小白看到 VS Code 那种“东一块西一块”的界面，估计也会发懵。&lt;/p&gt;
&lt;p&gt;因此 OpenClaw 支持 IM 聊天工具接入，对小白更加友好，直接把自己推到了风口浪尖。&lt;/p&gt;
&lt;p&gt;但是我们也需要看到，OpenClaw 接入 IM 也有一些妥协：比如我们交给他一些任务，我们肯定很想知道它是怎么做的，在干什么，但是微信、QQ 这些 IM 只会显示“正在输入中...”；放在 Claude Code 上，则会在输入框上显示自己的计划步骤、当前进行到了哪一步骤了等等。可见 OpenClaw 因为接入 IM，天生信息密度不足。&lt;/p&gt;
&lt;h3&gt;我所理解的模块整合&lt;/h3&gt;
&lt;p&gt;在 OpenClaw 之前，大家聊的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你用了什么 MCP？&lt;/li&gt;
&lt;li&gt;你用了什么 skill？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个时候，大家疯狂魔改自己的 Agent，已经出现了基于 RAG 的记忆系统、网络搜索工具和 Deep Research 工作流等工具，或者说 Agent 功能模块、设计思想。但是大家并不明确：如果你要做一个非常像人一样的 Agent，到底需要哪些模块？&lt;/p&gt;
&lt;p&gt;我认为 OpenClaw 的价值之一，是把模型、工具、消息渠道、记忆和 Skill 等模块放进了同一套可运行的个人助手框架。这里的“集成度更高”是我的评价，不代表这些模块由 OpenClaw 首创。&lt;/p&gt;
&lt;p&gt;比如第一，OpenClaw 使用工作区中的 Markdown 文件保存长期信息和指令，包括 &lt;code&gt;AGENTS.md&lt;/code&gt;、&lt;code&gt;USER.md&lt;/code&gt;、&lt;code&gt;SOUL.md&lt;/code&gt; 和 &lt;code&gt;MEMORY.md&lt;/code&gt; 等。新会话可以加载其中一部分内容，记忆搜索也可以按需找回相关记录。它提供了跨会话持久化的基础，但实际能否正确回忆仍取决于内容质量、检索和上下文预算。&lt;/p&gt;
&lt;p&gt;第二，是 skill。在我看来，其实 &lt;strong&gt;skill 的本质是软件&lt;/strong&gt;：程序、数据、文档，软件应有的它都有，只不过使用软件的是人，使用 skill 的是大模型。想一想，如果你拿到自己的新电脑，电脑什么软件都没有，你无法通过浏览器上网获取信息，没有 VS Code 给你拿来编程，也没有 QQ 微信给你用来跟朋友聊天...你是不是就想安装软件，给自己的电脑更多能力，使其更强大？&lt;/p&gt;
&lt;p&gt;OpenClaw 支持 Skill，也提供了 ClawHub 这样的分发入口。在我看来，这有点像把软件分发的思路放到 Agent 上：用户不必修改核心循环，也能按需增加一组工具和操作说明。&lt;/p&gt;
&lt;p&gt;记忆系统和 Skill 组合起来后，会给人一种“越用越熟悉”的感觉：文档可以保存需要跨会话保留的信息，Skill 可以把重复流程整理成按需加载的操作说明。不过这些机制不会天然保证信息正确或永不遗忘，仍然需要用户检查和维护。&lt;/p&gt;
&lt;p&gt;我倾向于认为，记忆和按需加载的 Skill 会继续成为个人 Agent 的常见模块。但这是一种趋势判断，不意味着所有 Agent 都必须采用相同的文件结构或实现方式。&lt;/p&gt;
&lt;h2&gt;Hermes Agent 又是什么？&lt;/h2&gt;
&lt;p&gt;OpenClaw 发布后，基于各种编程语言的模仿项目层出不穷：nanobot、picoclaw 等等。这里强烈推荐一下 nanobot，相对于 OpenClaw，它的代码更干净、功能基本全面、基于 Python 更适合魔改，非常适合个人用户使用。&lt;/p&gt;
&lt;p&gt;后来我又开始关注 Hermes Agent。它同样提供 CLI、消息渠道、工具、记忆与 Skill，并提供从 OpenClaw 迁移设置、记忆和用户 Skill 的命令。&lt;/p&gt;
&lt;p&gt;Hermes Agent 把自己描述为能够从使用中学习的 Agent。根据官方文档，它可以通过 &lt;code&gt;skill_manage&lt;/code&gt; 创建和更新 Skill，也把 Skill 定义为按需加载的程序性知识。&lt;/p&gt;
&lt;p&gt;这让我感兴趣的地方是：持久化的不只是“用户是谁、发生过什么”，还可以包括“下一次怎样完成这类任务”。不过，自动生成的 Skill 是否可靠，仍然取决于验证、权限边界和后续维护，不能把“能创建”直接等同于“会稳定地自我进化”。&lt;/p&gt;
&lt;p&gt;Hermes 还提供持久记忆、消息网关和 OpenClaw 迁移工具。它与 OpenClaw 有不少相似模块，但两者的实现、默认交互方式和安全边界并不完全相同，不能只用“更完善的 OpenClaw”概括。&lt;/p&gt;
&lt;p&gt;因此，我更愿意把 Hermes Agent 看作另一套值得对照阅读的 Agent 实现，而不是简单的替代关系。&lt;/p&gt;
&lt;h2&gt;我所认为的 Agent 未来的发展路线&lt;/h2&gt;
&lt;p&gt;总的来说，OpenClaw 让我更直观地看到，一个面向个人长期使用的 Agent 需要怎样连接消息渠道、执行环境、记忆和扩展能力。&lt;/p&gt;
&lt;p&gt;我更愿意把 OpenClaw 类比成连接模型、用户和本机工具的控制层：记忆保存长期信息，Skill 和插件扩展能力，消息渠道负责把它带到日常使用场景中。这个“操作系统”说法只是帮助理解的比喻。&lt;/p&gt;
&lt;p&gt;我认为 Agent 未来主要是朝下面几个方向发展：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生态圈形成：未来的 ReAct 循环尽可能简单，但是可以任意插入或替换自己想要的模块，形成模块生态；目前已经形成了 skill 生态。&lt;/li&gt;
&lt;li&gt;节省 Token：少 Token 也可以产生高质量输出。&lt;/li&gt;
&lt;li&gt;Agent + 机器人：让机器人也变得更加智能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;对我而言，OpenClaw 最值得关注的两点是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它让更多人通过日常聊天渠道接触可以调用工具的个人 Agent；&lt;/li&gt;
&lt;li&gt;它提供了一个可以具体讨论的模块组合，而不只是抽象的 ReAct 循环。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至于它是不是 Harness 的“最佳实践”，目前还没有足够统一的评价标准。更准确的说法是：OpenClaw 和 Hermes Agent 都提供了值得研究的公开实现，我会继续观察它们如何处理权限、失败恢复、上下文增长和长期记忆质量。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openclaw/openclaw&quot;&gt;OpenClaw 官方仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openclaw/openclaw/blob/main/docs/concepts/memory.md&quot;&gt;OpenClaw：Memory overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openclaw/openclaw/blob/main/docs/tools/skills.md&quot;&gt;OpenClaw：Skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hermes-agent-org/hermes&quot;&gt;Hermes Agent 官方仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hermes-agent-org/hermes/blob/main/website/docs/guides/work-with-skills.md&quot;&gt;Hermes Agent：Working with Skills&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>HTTP 请求方法：GET、POST、PUT 与幂等性</title><link>https://mrsibe.top/posts/http-explained/http-request-methods/</link><guid isPermaLink="true">https://mrsibe.top/posts/http-explained/http-request-methods/</guid><description>整理常见 HTTP 请求方法的标准语义：GET、HEAD、POST、PUT、DELETE、PATCH 和 OPTIONS 各自的用途，以及安全与幂等这两个属性在真实工程中意味着什么。</description><pubDate>Wed, 15 Apr 2026 09:16:00 GMT</pubDate><content:encoded>&lt;h2&gt;常见的请求方法&lt;/h2&gt;
&lt;p&gt;基础教程里经常介绍 GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE 和 PATCH，但 HTTP 方法并不只有固定的八种或九种。IANA 维护的 &lt;a href=&quot;https://www.iana.org/assignments/http-methods/&quot;&gt;HTTP Method Registry&lt;/a&gt; 还登记了 WebDAV 等扩展方法。&lt;/p&gt;
&lt;h3&gt;GET&lt;/h3&gt;
&lt;p&gt;GET 请求传输目标资源当前选中的表示。它最常用于读取页面或 API 资源，但“读取”不等于服务器内部完全没有副作用：服务端仍可能记录日志或统计访问量。&lt;/p&gt;
&lt;h3&gt;HEAD&lt;/h3&gt;
&lt;p&gt;HEAD 与 GET 的语义相同，但响应不传输消息内容。它适合在不下载完整内容时获取响应字段，例如检查资源类型、长度或缓存信息。&lt;/p&gt;
&lt;h3&gt;POST&lt;/h3&gt;
&lt;p&gt;POST 让目标资源按照自身语义处理请求内容。创建资源、提交表单、触发操作都可能使用 POST，但“POST 就是新增”只是常见的 API 设计习惯，不是它的完整定义。&lt;/p&gt;
&lt;h3&gt;PUT&lt;/h3&gt;
&lt;p&gt;PUT 请求使用消息内容创建或替换目标 URI 的当前表示。客户端通常已经知道要操作的 URI，这也是它与常见 POST 创建接口的重要区别。&lt;/p&gt;
&lt;h3&gt;DELETE、OPTIONS 与 PATCH&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;DELETE 请求删除目标资源与其当前功能之间的关联；&lt;/li&gt;
&lt;li&gt;OPTIONS 用于询问目标资源支持的通信选项；&lt;/li&gt;
&lt;li&gt;PATCH 用于对资源做部分修改，其语义由 &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc5789.html&quot;&gt;RFC 5789&lt;/a&gt; 定义。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安全与幂等&lt;/h2&gt;
&lt;p&gt;“安全”描述的是方法的既定语义基本只读，客户端没有请求改变服务器状态。RFC 9110 定义的 GET、HEAD、OPTIONS 和 TRACE 是安全方法。日志、计费统计等由服务器自行产生的副作用，不会因此改变方法的安全属性。&lt;/p&gt;
&lt;p&gt;“幂等”指多次发送相同请求，对服务器产生的预期效果与发送一次相同。安全方法以及 PUT、DELETE 都是幂等的；POST 通常不是。幂等关注的是客户端请求的预期效果，重复请求的响应内容、日志条数或版本历史仍可能不同。&lt;/p&gt;
&lt;p&gt;PATCH 在 IANA 注册表中不是幂等方法，但具体接口可以通过条件请求或业务约束提供额外保证。工程中判断能否自动重试时，不能只看方法名称，还要看接口约定。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;HTTP 方法没有“只有八种”这样的固定数量，扩展方法应查询 IANA 注册表；&lt;/li&gt;
&lt;li&gt;POST 是资源特定处理，PUT 是创建或替换目标 URI 的表示；&lt;/li&gt;
&lt;li&gt;安全和幂等描述的是标准语义与预期效果，不代表服务器内部绝对没有副作用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9110.html&quot;&gt;RFC 9110：HTTP Semantics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.iana.org/assignments/http-methods/&quot;&gt;IANA HTTP Method Registry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc5789.html&quot;&gt;RFC 5789：PATCH Method for HTTP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>HTTP/1.1 报文长什么样？从请求行到消息体</title><link>https://mrsibe.top/posts/http-explained/what-does-an-http-message-look-like/</link><guid isPermaLink="true">https://mrsibe.top/posts/http-explained/what-does-an-http-message-look-like/</guid><description>拆开一条 HTTP/1.1 请求和响应报文，逐段说明请求行、状态行、首部字段、空行与消息体分别承载什么信息，并解释 Host、Content-Length 等常见字段的作用。</description><pubDate>Wed, 15 Apr 2026 09:15:00 GMT</pubDate><content:encoded>&lt;p&gt;本文主要讨论 HTTP/1.1 的文本报文格式。HTTP/2 和 HTTP/3 使用不同的二进制分帧机制，不在本文范围内。&lt;/p&gt;
&lt;p&gt;HTTP/1.1 消息可以分为请求和响应：请求由客户端发送给服务器，响应由服务器返回给客户端。两者都由起始行、若干字段、一个空行和可选的消息体组成。请求的起始行是请求行，响应的起始行是状态行。&lt;/p&gt;
&lt;h2&gt;请求行&lt;/h2&gt;
&lt;p&gt;请求行由三部分构成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;请求方法：是一个动词，如 GET/POST，表示对资源的操作；&lt;/li&gt;
&lt;li&gt;请求目标：通常是一个 URI，标记了请求方法要操作的资源；&lt;/li&gt;
&lt;li&gt;版本号：表示报文使用的 HTTP 协议版本。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这三个部分通常使用空格来分隔，最后要用 CRLF 换行表示结束。&lt;/p&gt;
&lt;h2&gt;状态行&lt;/h2&gt;
&lt;p&gt;比起请求行来说，状态行要简单一些，同样也是由三部分构成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;版本号：表示报文使用的 HTTP 协议版本；&lt;/li&gt;
&lt;li&gt;状态码：用三位数代码的形式表示处理的结果，比如 200 是成功，500 是服务器错误；&lt;/li&gt;
&lt;li&gt;原因：作为数字状态码补充，是更详细的解释文字，帮助人理解原因。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;字段&lt;/h2&gt;
&lt;p&gt;请求行或状态行再加上头部字段集合就构成了 HTTP 报文里完整的请求头或响应头。头部字段都是键值对，key 和 value 之间用“:”分隔，最后用 CRLF 换行表示字段结束。&lt;/p&gt;
&lt;p&gt;HTTP 字段很灵活。标准定义了 Host、Content-Length 等常用字段，也允许扩展字段。一个字段可以出现在哪类消息、是否允许重复，以及具体语义，都应以该字段的定义为准，不宜简单套用固定的四分类。&lt;/p&gt;
&lt;h3&gt;Host&lt;/h3&gt;
&lt;p&gt;它属于请求字段，只能出现在请求头里，也是唯一一个 HTTP/1.1 规范里要求&lt;strong&gt;必须出现&lt;/strong&gt;的字段。如果请求头里没有 Host，那这就是一个错误的报文。&lt;/p&gt;
&lt;h3&gt;User-Agent&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;User-Agent&lt;/strong&gt;是请求字段，只出现在请求头里，用于表明客户端的类型。不过大家都相互伪装，导致这个字段非常混乱。&lt;/p&gt;
&lt;h3&gt;Date&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Date&lt;/strong&gt;字段是一个通用字段，但通常出现在响应头里，表示 HTTP 报文创建的时间，客户端可以使用这个时间再搭配其他字段决定缓存策略。&lt;/p&gt;
&lt;h3&gt;Server&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Server&lt;/strong&gt;字段是响应字段，只能出现在响应头里。它告诉客户端当前正在提供 Web 服务的软件名称和版本号。&lt;/p&gt;
&lt;h3&gt;Content-Length&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Content-Length&lt;/strong&gt; 表示消息体的长度，单位是字节。不过，没有 Content-Length 并不等于一定使用 chunked。&lt;/p&gt;
&lt;p&gt;在 HTTP/1.1 中，消息体边界要按一组有优先级的规则判断：有些响应根据请求方法或状态码本来就没有消息体；使用 Transfer-Encoding 且最终编码为 chunked 时，接收方通过读取各个 chunk 判断结束；也可能由 Content-Length 给出长度。请求既没有 Transfer-Encoding 也没有 Content-Length 时，消息体长度通常为零；部分响应还可以由连接关闭来界定消息体。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本文讨论的 HTTP/1.1 消息可以理解为“起始行 + 字段 + 空行 + 可选的消息体”；&lt;/li&gt;
&lt;li&gt;HTTP 报文可以没有 body，但必须要有 header，而且 header 后也必须要有空行；&lt;/li&gt;
&lt;li&gt;请求头由“请求行+头部字段”构成，响应头由“状态行+头部字段”构成；&lt;/li&gt;
&lt;li&gt;请求行有三部分：请求方法，请求目标和版本号；&lt;/li&gt;
&lt;li&gt;状态行也有三部分：版本号，状态码和原因字符串；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9112.html&quot;&gt;RFC 9112：HTTP/1.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9110.html&quot;&gt;RFC 9110：HTTP Semantics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>我的业余无线电入门记录：从能力验证到设置电台</title><link>https://mrsibe.top/posts/amateur-radio-beginner-guide/</link><guid isPermaLink="true">https://mrsibe.top/posts/amateur-radio-beginner-guide/</guid><description>记录我从零开始学习业余无线电、参加操作技术能力验证、通过考试到购买设备并设置电台的完整过程，也整理了入门阶段的学习重点、报名渠道与备考资料。</description><pubDate>Wed, 15 Apr 2026 02:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;最后核对日期：2026-08-29。本文记录个人入门经历，不替代当地无线电管理机构的办事指南。能力验证、许可材料和办理方式可能因地区及政策更新而变化，请在操作发射设备前核对官方要求。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;初识业余无线电&lt;/h2&gt;
&lt;p&gt;相信每个热爱美剧的小伙伴都看过《怪奇物语》。其中有一个片段是：技术狂魔达斯汀带着小伙伴们去山顶联络自己的“女朋友”苏珊。他们带着一大堆铁架子，在山顶上架起了几米高的无线电天线，最后竟误打误撞发现了地下的苏联基地。&lt;/p&gt;
&lt;p&gt;这段剧情虽然有些夸张，但是也展现了业余无线电的魅力：通过无线电波的传播，两个人即使看不见对方，也可以相互沟通。除此之外，无线电的沟通也非常有趣：CQ、CQ、CQ，这里是 Bravo Delta Eight...无线电的通联往往会涉及到很多让人摸不着头脑的词语，尤其是古早电影中的发报员，通过滴答滴答的莫尔斯电码发送信息，更是给无线电这个领域蒙上了一层奇幻的色彩。&lt;/p&gt;
&lt;p&gt;实际上，业余无线电没有那么迷幻。说白了，业余无线电就是一群热爱无线电的爱好者，通过无线电技术进行通信、娱乐、竞赛、试验等等。&lt;/p&gt;
&lt;h2&gt;业余无线电做什么？&lt;/h2&gt;
&lt;p&gt;刚刚提到的，业余无线电爱好者（简称 HAM）大部分时间都是在通联，也就是对话。大家通常会在对话中交换自己的个人信息，例如呼号（业余无线电领域的“车牌号”）、无线电设备和所在位置，并把这些内容记录在自己的“通联手册”上。想一想，一个 HAM 给另一个 HAM 打开自己的通联手册，上面全是密密麻麻的通联日志，他该多么自豪呀！&lt;/p&gt;
&lt;p&gt;除了通联手册，HAM 通联结束后还经常交换 QSL 卡片。QSL 是 HAM 自制的类似于明信片的卡片，记录通联的信息，往往还有各种各样好看的图片图案，各具特色。收集 QSL 和集邮一样，也成为了不少 HAM 的爱好。&lt;/p&gt;
&lt;p&gt;虽然叫业余无线电，但是 HAM 其实都并不业余。业余指的是相对于海事、航空和军事等领域，不存在专用性，只是为了爱好而服务，并不代表 HAM 的技术不好。有的资深 HAM 可以通过短波电台进行上千公里的通联，甚至向月球发射无线电信号测量地月距离。&lt;/p&gt;
&lt;p&gt;无线电波是无情的，但是 HAM 是有情的。2008 年汶川大地震发生几分钟后，HAM 迅速组建应急无线电台，为灾区提供应急通信服务，调动车辆数万台次，为救灾活动提供了顽强的通信保障。&lt;/p&gt;
&lt;h2&gt;如何入门无线电？&lt;/h2&gt;
&lt;p&gt;入门无线电，最重要的是知识和资质。&lt;/p&gt;
&lt;h3&gt;如何学习无线电相关的知识&lt;/h3&gt;
&lt;p&gt;业余无线电台的操作其实很简单，但是要深入去做一些很有挑战性的事情，需要大量的技术知识积累。&lt;/p&gt;
&lt;p&gt;现在可以通过 B 站、社区和本地协会等渠道了解业余无线电，但碎片信息容易遗漏适用条件。如果想系统学习，可以参考童效勇、陈方主编的《业余无线电通信》，再结合现行法规、最新版题库和实际设备手册交叉核对。&lt;/p&gt;
&lt;h3&gt;资质&lt;/h3&gt;
&lt;p&gt;无线电频谱是一种公共资源，设置、使用业余无线电台需要遵守无线电管理规定。按照现行《业余无线电台管理办法》，申请人需要通过相应的操作技术能力验证，并依法取得&lt;strong&gt;业余无线电台执照&lt;/strong&gt;后再设置、使用电台。&lt;/p&gt;
&lt;p&gt;操作技术能力验证分为 A、B、C 类，不同类别对应不同的知识要求。实际可以使用的频率、功率和设置区域，应以能力验证类别、无线电台执照载明参数及现行规定为准。升级条件和验证标准可能调整，不要只依赖旧文章中的经验。&lt;/p&gt;
&lt;p&gt;能力验证解决的是“是否具备相应操作技术能力”，电台执照解决的是“这套电台是否获准设置、使用”。申请材料、呼号和执照参数由无线电管理机构依法审核，二者不能互相替代。&lt;/p&gt;
&lt;h2&gt;如何参加操作技术能力验证？&lt;/h2&gt;
&lt;h3&gt;准备相关知识&lt;/h3&gt;
&lt;p&gt;操作技术能力验证有公开题库。本文核对时的版本是 &lt;a href=&quot;https://www.crac.org.cn/?p=1287&quot;&gt;业余无线电台操作技术能力验证题库（2025 年版）&lt;/a&gt;，其中分别提供 A、B、C 类题库。题库可能继续更新，准备时应从 CRAC 官方页面重新确认版本。&lt;/p&gt;
&lt;p&gt;准备相关知识环节建议把大量时间花在背题库上。其次在《业余无线电通信》这本书的附录有 A 类题库的考试大纲，非常方便新人记忆。（本人基本上看一遍大纲刷几遍题就满分通过了）&lt;/p&gt;
&lt;h3&gt;考试报名&lt;/h3&gt;
&lt;p&gt;验证安排和报名入口由各地无线电管理机构或受委托单位发布，有些地区也会通过“智谱”等平台提供信息。应以所在地最新通知为准，不同地区的报名时间、材料和领取方式可能不同。&lt;/p&gt;
&lt;p&gt;报名需要个人的电子证件照、身份证等其他个人信息。&lt;/p&gt;
&lt;h3&gt;考试&lt;/h3&gt;
&lt;p&gt;报名成功后，需要在规定时间到考试地点参加考试。一切遵照当地无线电协会或管理局安排即可。&lt;/p&gt;
&lt;h3&gt;查询验证结果&lt;/h3&gt;
&lt;p&gt;成绩查询、能力验证证明的形式和领取时间由当地安排决定。通过验证后，如果准备设置、使用自己的电台，还需要继续按所在地办事指南申请业余无线电台执照。&lt;/p&gt;
&lt;h2&gt;如何设置业余无线电台？&lt;/h2&gt;
&lt;h3&gt;购买设备&lt;/h3&gt;
&lt;p&gt;新手通常会从手持电台开始。我使用和关注过泉盛 UV-K6、宝锋 UV-5R、灵通 LT-6100Plus 等设备，但下面只是个人选购记录，不是通用购买结论。&lt;/p&gt;
&lt;p&gt;购买前要核对设备的型号核准信息和可发射频率范围。对自制、改装、拼装等未取得型号核准的发射设备，现行办法要求其符合国家标准和无线电管理规定，申请设台时还可能需要技术检测。能够刷固件不等于可以在任意频率、功率下发射。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我了解过的几款入门设备：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;泉盛 UV-K6&lt;/strong&gt;：价格相对容易接受，也有较多第三方固件资料。刷写固件前要了解风险，发射时仍需遵守核准参数和当地规定&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;宝锋 UV-5R&lt;/strong&gt;：常见的入门手持台，资料和配件比较容易找到&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灵通 LT-6100Plus&lt;/strong&gt;：提供功率调节等常见功能，购买前可以结合实际需求比较&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;常见配件：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;备用电池&lt;/li&gt;
&lt;li&gt;与频段和接口匹配的天线&lt;/li&gt;
&lt;li&gt;充电器&lt;/li&gt;
&lt;li&gt;车载充电器（如果需要在车上使用）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;准备设台资料&lt;/h3&gt;
&lt;p&gt;一般需要设备的铭牌、个人居住地等信息。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;如果打算实际发射，可以把流程概括为：&lt;strong&gt;学习无线电知识 → 通过相应的操作技术能力验证 → 按规定申请业余无线电台执照 → 在执照参数范围内操作&lt;/strong&gt;。设备价格和办理方式会变化，不适合用一个固定金额概括。&lt;/p&gt;
&lt;p&gt;业余无线电不仅是通信工具，更是连接全球爱好者的桥梁。通过无线电，你可以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;结识世界各地的 HAM 朋友&lt;/li&gt;
&lt;li&gt;参与应急通信保障服务社会&lt;/li&gt;
&lt;li&gt;探索无线电波传播的奥秘&lt;/li&gt;
&lt;li&gt;参加各种无线电竞赛活动&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你对业余无线电感兴趣，可以先从现行管理办法、所在地通知和最新版题库开始，确认自己准备操作的设备与流程都符合要求。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.miit.gov.cn/gyhxxhb/jgsj/cyzcyfgs/bmgz/wxdl/art/2024/art_9a2dba77d86942cebd1e9cbacd2d4b20.html&quot;&gt;工业和信息化部《业余无线电台管理办法》（第 67 号令）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.miit.gov.cn/zwgk/zcjd/art/2024/art_6314cfd378544eb6b94606432f527d02.html&quot;&gt;工业和信息化部：《业余无线电台管理办法》解读&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.crac.org.cn/?p=1287&quot;&gt;CRAC：业余无线电台操作技术能力验证题库（2025 年版）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>我用 Headscale 连接宿舍与外部设备</title><link>https://mrsibe.top/posts/tailscale-headscale-home-lab/</link><guid isPermaLink="true">https://mrsibe.top/posts/tailscale-headscale-home-lab/</guid><description>记录我用 Headscale 搭建自建内网穿透方案的过程：比较常见方案的取舍，说明为什么选择 Headscale，以及如何为宿舍 NAS 部署服务端并用 Tailscale 客户端连接不同设备。</description><pubDate>Mon, 13 Apr 2026 02:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;最后核对日期：2026-08-29。Headscale 与 Tailscale 的配置项会随版本变化，部署时请同时查看对应版本的官方文档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大家好，我是西贝。&lt;/p&gt;
&lt;p&gt;我用一台闲置的老 PC 搭建了一个 NAS：用 Immich 备份照片，用 Navidrome 搭建 HiFi 音乐服务器，偶尔还会启动 MySQL、Redis 测试项目。&lt;/p&gt;
&lt;p&gt;但我经常出校，一旦离开校园网，就无法访问内网里的 NAS：因为 NAS 只有内网地址，外网无法直接&quot;看到&quot;它。&lt;/p&gt;
&lt;p&gt;打个比方：校园网就像一栋公寓楼，每台设备是一个房间，内网 IP 是房间号。你在外面想找人，只知道&quot;某某公寓&quot;的地址（公网 IP），却不知道房间号，而且公寓大门（NAT 网关）默认不让外人随便进。所以，必须要有某种方式&quot;绕过&quot;这扇门——这就是内网穿透。&lt;/p&gt;
&lt;h2&gt;内网穿透有哪些方案？&lt;/h2&gt;
&lt;p&gt;市面上的内网穿透方案大致分两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;端口映射类&lt;/strong&gt;（如 frp）：把内网的某个服务暴露到公网，适合对外提供服务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组网类&lt;/strong&gt;（如 WireGuard、Tailscale）：在多台设备之间搭建虚拟局域网，注册设备之间可以互相访问，更适合自用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而我毫无疑问选择了第二种。想一想自己所有的电子设备都可以 ping 通，实现万物互联，实在是一件美事。&lt;/p&gt;
&lt;p&gt;最开始我使用的是 WireGuard 搭建 VPN，但是配置非常麻烦。而 Tailscale 就解决了这个问题，还多出了不少非常有用的功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MagicDNS&lt;/strong&gt;：可以给自己的虚拟局域网和设备安排一个易记的域名（如 &lt;code&gt;nas.ts.net&lt;/code&gt;），不用背内网 IP 地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子网路由&lt;/strong&gt;：在一台设备上配置子网路由，就能让整个局域网的设备都被 Tailscale 网络访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exit Node&lt;/strong&gt;：可以让所有流量通过某个节点出去，类似代理。比如我在咖啡馆连公共 WiFi，可以让流量通过家里的 NAS 出去，更安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;我的解决方案&lt;/h2&gt;
&lt;p&gt;最终，我的方案是：&lt;strong&gt;Headscale + Tailscale 客户端&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Tailscale 官方提供的服务很方便，但我选择了自建控制服务器（Headscale）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;隐私可控&lt;/strong&gt;：设备列表、连接信息都在自己的服务器上，不经过第三方&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无限制&lt;/strong&gt;：官方免费版有设备数限制（虽然限制高达上百设备，完全够用）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;国内访问更快&lt;/strong&gt;：官方服务器在国外，偶尔抽风；阿里云 ECS 在国内，延迟更低&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Headscale 是 Tailscale 控制服务器的开源实现，完全兼容 Tailscale 官方客户端——这意味着我可以享受 Tailscale 优秀的客户端体验，同时掌控自己的数据。&lt;/p&gt;
&lt;p&gt;其次，Headscale 内嵌了 derp 服务，在遇到对称性 NAT 打洞失败的时候，通过 derp 来做兜底。&lt;/p&gt;
</content:encoded></item><item><title>我在 MacBook 上常用的开发与效率软件</title><link>https://mrsibe.top/posts/macbook-software-recommendations/</link><guid isPermaLink="true">https://mrsibe.top/posts/macbook-software-recommendations/</guid><description>记录我在 MacBook 上实际长期使用的软件：包括开发工具、AI 助手与知识管理、系统增强和效率工具，以及办公与影音娱乐类应用，并说明它们各自的用途。</description><pubDate>Wed, 28 Jan 2026 03:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;最后核对日期：2026-08-29。软件功能、价格和系统兼容性可能变化，请以各项目官网为准。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;开发工具&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VSCode&lt;/strong&gt;：我目前主要使用的代码编辑器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google Chrome&lt;/strong&gt;：我主要使用的浏览器，也方便调试网页&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iTerm2&lt;/strong&gt;：我目前常用的 macOS 终端模拟器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OrbStack&lt;/strong&gt;：我用来管理 Docker 容器，也支持创建 Linux 虚拟机和管理 Kubernetes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CC Switch&lt;/strong&gt;：灵活切换 Claude/Codex 等编程大模型 API&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;AI 助手与知识管理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Alma&lt;/strong&gt;：强大的 Agent 客户端。我很喜欢把它当作助理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cherry Studio&lt;/strong&gt;：功能非常多的大模型客户端，我之前特别喜欢用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Obsidian&lt;/strong&gt;：笔记软件，插件非常强大。折腾过笔记软件的肯定都用过&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;极速说&lt;/strong&gt;：语音编程神器，解放双手&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;系统增强与效率工具&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Raycast&lt;/strong&gt;：我用来替代 Spotlight 的启动器，也会使用它的扩展功能&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;鼠须管 (Rime)&lt;/strong&gt;：自定义空间很大的输入法（通过 iCloud 解决跨设备同步）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ice&lt;/strong&gt;：极简的顶栏图标管理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BetterDisplay&lt;/strong&gt;：用于管理外接显示器和 HiDPI 设置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stats&lt;/strong&gt;：实时监控 CPU/GPU/网络状态，开源的很好用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AlDente&lt;/strong&gt;：通过限制充电阈值来延长电池寿命&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PearCleaner&lt;/strong&gt;：轻量、开源的软件卸载残余清理工具。卸载的时候一定要再三确认！&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;办公与影音娱乐&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OnlyOffice&lt;/strong&gt;：兼顾美观与兼容性的开源 Office 方案&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IINA&lt;/strong&gt;：非常强大的播放器&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>《纳瓦尔宝典》阅读笔记：阅读、选书与输出</title><link>https://mrsibe.top/posts/naval-ravikant-reading-notes/</link><guid isPermaLink="true">https://mrsibe.top/posts/naval-ravikant-reading-notes/</guid><description>记录我读《纳瓦尔宝典》时关于阅读与学习方法的思考：为什么要海量阅读、读不进去时怎么办、怎样找到并读好一本书，以及除了阅读还应该做什么。</description><pubDate>Mon, 19 Jan 2026 03:11:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是我阅读《纳瓦尔宝典》的阅读笔记，主要是把书中自己觉得很有意思的部分，用自己的话复述一遍。强烈建议阅读《纳瓦尔宝典》原著&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;为什么要海量阅读？&lt;/h2&gt;
&lt;p&gt;首先阅读是&lt;strong&gt;获取知识的有效途径&lt;/strong&gt;。比如阅读科学、哲学、博弈论、心理学等领域的一些经典著作，能接触一些之前可能从未接触过的新知识。这些新知识往往会给自己带来顿悟和震撼。&lt;/p&gt;
&lt;p&gt;其次我的高中语文老师说，一个人的人生体验是有限的，但是通过阅读可以&lt;strong&gt;去体会不同的人生&lt;/strong&gt;，我觉得很有道理。阅读小说或者别人的传记，才知道自己的行为会带来哪些风险和机遇。治身之道，就在书中。&lt;/p&gt;
&lt;h2&gt;阅读文字好难，怎么办？&lt;/h2&gt;
&lt;p&gt;现在互联网的文字快餐大家都吃惯了，静下心阅读一本书确实变成了一件难事。所以只有&lt;strong&gt;把读书变成爱好&lt;/strong&gt;，才能坚持阅读下去。&lt;/p&gt;
&lt;p&gt;培养读书习惯的最好办法就是&lt;strong&gt;读自己感兴趣的书&lt;/strong&gt;。只有喜欢读，才会继续读。这几天我对心理学笔记感兴趣，就让 Gemini 帮我推荐几本，然后去微信读书上读。正因为感兴趣，我读书的欲望就更大了，坐地铁的时候读，吃饭的时候读，回家了继续读。一天一本，一周可以看好几本书。&lt;/p&gt;
&lt;h2&gt;怎样才能找到好书？怎么读一本好书？&lt;/h2&gt;
&lt;p&gt;首先需要强调一点：&lt;strong&gt;不是所有的书都是好书！&lt;/strong&gt; 因此我们需要尽可能地排除掉不好的书。&lt;/p&gt;
&lt;p&gt;在阅读之前，可以让 Ai 推荐一些比较经典的书，还可以去豆瓣上看看评分高的书。这些书往往是经过大家检验的经典。尤其是那种经历了几百年还被人推荐的书，能存世如此之久，说明这本书有它存在的价值。&lt;/p&gt;
&lt;p&gt;在阅读的时候，把握下面几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先&lt;strong&gt;速读&lt;/strong&gt;，如果第一章的内容看不下去，就随便看看后面几章；后面&lt;strong&gt;还是看不下去，这本书就该丢掉&lt;/strong&gt;：对于你来说这本书不算好书。&lt;/li&gt;
&lt;li&gt;读了觉得还不错，说明这很可能是本好书。这个时候放慢节奏，慢慢品读。&lt;strong&gt;好书应该仔细品味其中精髓，纳入自己的心智&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;除了阅读还有什么要做的&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;输出！不断的输出！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;根据费曼学习法，只吸收书本中的知识，不一定掌握了其中的精髓。只有不断的输出，或者说“教学相长”，才能真正掌握知识。&lt;/p&gt;
&lt;p&gt;因此就像本文一样，阅读完《纳瓦尔宝典》这本书，把自己学到的东西输出为一篇文章，才能真正让习得的知识在大脑里面结构化，而后才能做到知行合一。&lt;/p&gt;
&lt;p&gt;这里是西贝茶馆，欢迎点赞、留言加关注！&lt;/p&gt;
</content:encoded></item><item><title>终端、Shell 与内核之间是什么关系？</title><link>https://mrsibe.top/posts/terminal-shell-and-kernel/</link><guid isPermaLink="true">https://mrsibe.top/posts/terminal-shell-and-kernel/</guid><description>从一次命令输入开始，梳理终端模拟器、Shell、系统调用与内核之间的关系：内核如何管理系统资源、Shell 如何读取并执行命令，以及我们为什么依然需要终端这个黑窗口。</description><pubDate>Sun, 28 Dec 2025 07:59:00 GMT</pubDate><content:encoded>&lt;p&gt;对于计算机专业的学生来说，最具有仪式感的时刻，莫过于学长学姐神情凝重地对你说：“来，打开&lt;strong&gt;终端&lt;/strong&gt;，输入这行命令……”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://r2.mrsibe.top/2025/12/20251228155912601.webp&quot; alt=&quot;ghostty终端&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那一刻，屏幕上跳出一个漆黑的窗口，闪烁着五颜六色的字符。你可能会疑惑：在图形化界面如此发达的今天，为什么很多计算机专业人士依然痴迷于这种简陋的交互方式？这个黑窗口背后，到底隐藏着怎样的秘密？&lt;/p&gt;
&lt;h2&gt;内核：管理系统资源&lt;/h2&gt;
&lt;p&gt;要理解这一切，得先从计算机的“权力中心”说起。&lt;/p&gt;
&lt;p&gt;计算机运行着的每一个程序都需要资源：运行需要内存，存储需要磁盘，计算需要 CPU。然而，资源是有限的。如果每个程序都随心所欲地抢占内存、写入磁盘，系统瞬间就会陷入崩溃。&lt;/p&gt;
&lt;p&gt;于是，&lt;strong&gt;操作系统&lt;/strong&gt;应运而生。它的本质是一个“计算机资源管家”，而其中最核心的部分，被称为&lt;strong&gt;操作系统内核&lt;/strong&gt;，简称&lt;strong&gt;内核&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;内核运行在受保护的环境中，负责进程调度、内存管理、文件系统和设备等工作。普通应用程序不能随意操作这些资源，而是通过系统调用请求内核提供服务。&lt;/p&gt;
&lt;h2&gt;Shell：读取并执行命令&lt;/h2&gt;
&lt;p&gt;既然内核如此高冷，我们该如何指挥计算机干活呢？&lt;/p&gt;
&lt;p&gt;Shell 是命令解释器。它读取并解析我们输入的命令，处理变量展开、重定向和管道等语法，再决定如何执行命令。Shell 并不是所有程序访问内核的必经之路：图形界面应用、后台服务和其他程序同样可以通过库函数和系统调用使用内核服务。&lt;/p&gt;
&lt;p&gt;当你输入 &lt;code&gt;mkdir photos&lt;/code&gt; 时，大致会经历下面的过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;终端模拟器把输入交给 Shell；&lt;/li&gt;
&lt;li&gt;Shell 解析命令，判断它是函数、内建命令还是外部程序；&lt;/li&gt;
&lt;li&gt;对于 &lt;code&gt;mkdir&lt;/code&gt; 这样的外部命令，Shell 会按搜索规则找到并启动对应的可执行程序；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkdir&lt;/code&gt; 程序再通过库函数和系统调用，请求内核创建目录。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;像 &lt;code&gt;cd&lt;/code&gt; 这样需要改变 Shell 自身工作目录的命令，通常由 Shell 以内建命令的形式直接执行。内建命令和外部程序看起来都像是在“运行一条命令”，实现方式却不完全相同。&lt;/p&gt;
&lt;h2&gt;终端：运行 Shell 程序的黑窗口&lt;/h2&gt;
&lt;p&gt;那么，那个黑黑的窗口又是什么？为什么它叫&lt;strong&gt;终端&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;这要回到计算机的发展历史来讲。在上个世纪，计算机是巨大的昂贵设备，操作不便。程序员主要通过一台只有键盘和显示器的物理设备连接到主机上，这个设备就是终端。&lt;/p&gt;
&lt;p&gt;现在的个人计算机通常使用&lt;strong&gt;终端模拟器（Terminal Emulator）&lt;/strong&gt;。它负责显示字符、接收键盘输入，并把输入输出交给其中运行的 Shell。终端和 Shell 是两个不同的程序，只是日常使用时经常一起出现。&lt;/p&gt;
&lt;h2&gt;为什么我们依然需要这个“黑窗口”？&lt;/h2&gt;
&lt;p&gt;现在的操作系统，比如 Windows、macOS，都有着成熟的图形界面（GUI），终端看起来反而不够直观。但在开发、运维和批量处理任务中，命令行仍有自己的优势。&lt;/p&gt;
&lt;p&gt;首先是适合重复和批量操作。创建一个目录只需要 &lt;code&gt;mkdir 目录名&lt;/code&gt;；如果要按规则处理许多文件，也可以把步骤写成脚本重复执行。图形界面更适合直观探索，命令行则更容易复现、组合和自动化，两者并不是互相取代的关系。&lt;/p&gt;
&lt;p&gt;其次是 &lt;strong&gt;Shell 的强大能力&lt;/strong&gt;。Shell 拥有强大的“管道”机制。你可以把 A 命令的输出直接塞给 B 命令，再把 B 的结果传给 C。这种自由组合，能让你在几分钟内搭建出一个复杂的自动化处理流程。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在我第一次接触到终端的时候，觉得这个家伙实在是太简陋了；后来用多了终端，才发现终端的效率其实非常高，怪不得很多程序员喜欢使用。&lt;/p&gt;
&lt;p&gt;这篇文章先梳理到这里。后面我还会继续整理进程、系统调用和终端实现相关的内容。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/bash/manual/html_node/Command-Search-and-Execution.html&quot;&gt;GNU Bash：Command Search and Execution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/bash/manual/html_node/Shell-Builtin-Commands.html&quot;&gt;GNU Bash：Shell Builtin Commands&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man2/syscalls.2.html&quot;&gt;Linux manual page：system calls&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Git | 约定式提交规范</title><link>https://mrsibe.top/posts/git/conventional-commits/</link><guid isPermaLink="true">https://mrsibe.top/posts/git/conventional-commits/</guid><description>整理 Conventional Commits 约定式提交规范：说明提交信息的标准格式与各部分含义，列出我常用的提交类型，并给出可以直接参考的示例。</description><pubDate>Tue, 18 Nov 2025 09:26:35 GMT</pubDate><content:encoded>&lt;h2&gt;模版&lt;/h2&gt;
&lt;p&gt;注：尖括号的是必须出现的元素，方括号是可选的。所有的内容必须是英文。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;类型&amp;gt;[范围]: &amp;lt;描述&amp;gt;
[正文]
[脚注]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常见类型&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;fix&lt;/strong&gt;：修复了一个 bug&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;feat&lt;/strong&gt;：新增了一个功能&lt;/li&gt;
&lt;li&gt;build: 用于修改项目构建系统，例如修改依赖库、外部接口或者升级 Node 版本等；&lt;/li&gt;
&lt;li&gt;chore: 用于对非业务性代码进行修改，例如修改构建流程或者工具配置等；&lt;/li&gt;
&lt;li&gt;ci: 用于修改持续集成流程，例如修改 Travis、Jenkins 等工作流配置；&lt;/li&gt;
&lt;li&gt;docs: 用于修改文档，例如修改 README 文件、API 文档等；&lt;/li&gt;
&lt;li&gt;style: 用于修改代码的样式，例如调整缩进、空格、空行等；&lt;/li&gt;
&lt;li&gt;refactor: 用于重构代码，例如修改代码结构、变量名、函数名等但不修改功能逻辑；&lt;/li&gt;
&lt;li&gt;perf: 用于优化性能，例如提升代码的性能、减少内存占用等；&lt;/li&gt;
&lt;li&gt;test: 用于修改测试用例，例如添加、删除、修改代码的测试用例等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;fix 和 feat 是最常用的两个类型，必须记住。&lt;/p&gt;
&lt;h2&gt;示例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;feat: allow provided config object to extend other configs
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;feat: allow provided config object to extend other configs
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;fix: prevent racing of requests

Introduce a request id and a reference to latest request. Dismiss
incoming responses other than from latest request.

Remove timeouts which were used to mitigate the racing issue but are
obsolete now.

Reviewed-by: Z
Refs: #123
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文献&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.conventionalcommits.org/zh-hans/v1.0.0/&quot;&gt;约定式提交&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;更详细的规范可以去这个网站查看。&lt;/p&gt;
</content:encoded></item><item><title>Git | Git 和 GitHub 是什么</title><link>https://mrsibe.top/posts/git/what-are-git-and-github/</link><guid isPermaLink="true">https://mrsibe.top/posts/git/what-are-git-and-github/</guid><description>用几个简单场景解释 Git 为什么需要版本管理和分支，远程仓库解决了什么问题，以及 GitHub 在这套工作流里扮演的角色和它提供的协作能力。</description><pubDate>Mon, 17 Nov 2025 04:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 Git&lt;/h2&gt;
&lt;p&gt;Git 是一个分布式版本控制软件。&lt;/p&gt;
&lt;h2&gt;你为什么需要 Git&lt;/h2&gt;
&lt;h3&gt;版本管理&lt;/h3&gt;
&lt;p&gt;假设你有一天打算给项目添加新的代码，你噼里啪啦敲了一大堆代码，然后发现代码有问题，项目运行不起来了，被玩坏啦。&lt;/p&gt;
&lt;p&gt;这时候你就需要返回到之前那个能稳定运行的版本。Git 通过一个命令就可以直接返回到之前那个版本，这就是版本管理的好处。&lt;/p&gt;
&lt;h3&gt;分支管理&lt;/h3&gt;
&lt;p&gt;假设项目需要两个新功能：小张负责功能 A，小王负责功能 B。&lt;/p&gt;
&lt;p&gt;如果没有 Git，那可能需要考虑：小张先把 A 的功能写完后，把代码交给小王，再开发功能 B。这样存在最大的弊端：一是效率低下，两个功能没有“并行”开发，而是“串行”的。&lt;/p&gt;
&lt;p&gt;分支管理是这样解决的：项目的源代码叫做 main 分支，小张新建一个 feat/A 分支，这个分支就像 main 分支的&quot;双胞胎&quot;，但两人之后会长得不一样。小王则新建一个 feat/B 分支。从此以后，小张只修改 feat/A 分支的代码，小王修改 feat/B 分支的代码。现在就存在三个分支：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有新功能的 main 分支&lt;/li&gt;
&lt;li&gt;存在 A 功能的 feat/A 分支&lt;/li&gt;
&lt;li&gt;存在 B 功能的 feat/B 分支。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假如 A 功能开发完了，就把 feat/A 分支合并（merge）到 main 分支里面，使得 main 分支具有 A 功能。B 功能也是一样的原理。&lt;/p&gt;
&lt;p&gt;合并（merge）的原理是：找出 feat/A 分支和 main 分支的“共同祖先”，再把不同的代码应用到这个“共同祖先”上。&lt;/p&gt;
&lt;p&gt;通过分支管理，就可以做到两个功能可以同时开发，大大提高了开发效率（当然优点不止这个）。&lt;/p&gt;
&lt;h2&gt;什么是 GitHub&lt;/h2&gt;
&lt;p&gt;GitHub 是一个在线软件源代码托管服务平台，用于公开程序或软件的代码，使用 Git 作为版本控制软件。&lt;/p&gt;
&lt;h2&gt;为什么需要 GitHub&lt;/h2&gt;
&lt;h3&gt;托管 Git 中的代码&lt;/h3&gt;
&lt;p&gt;刚刚 Git 解决了版本管理和分支管理的问题，但是如果一个公司要多人协作，就需要一台服务器存储 main 分支的代码，然后大家访问这个服务器，从 main 分支上新增自己的分支。&lt;/p&gt;
&lt;p&gt;这个服务器可以自己搭建，不过现在很多项目会使用 GitHub：它是一个代码托管与协作平台，托管着大量公开和私有仓库。&lt;/p&gt;
</content:encoded></item><item><title>CS 61B | Defining and Using Classes</title><link>https://mrsibe.top/posts/cs-61b/02-defining-and-using-classes/</link><guid isPermaLink="true">https://mrsibe.top/posts/cs-61b/02-defining-and-using-classes/</guid><description>CS 61B 学习笔记：围绕 Java 中类的定义与使用，区分静态方法和实例方法，讲解构造函数、数组、静态变量以及 main 函数和命令行参数的用法。</description><pubDate>Mon, 03 Nov 2025 02:56:24 GMT</pubDate><content:encoded>&lt;h2&gt;静态方法和非静态方法&lt;/h2&gt;
&lt;p&gt;概念解释：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;猴子&lt;/th&gt;
&lt;th&gt;猴子 20 岁了&lt;/th&gt;
&lt;th&gt;猴子偷玉米&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;对于 OOP&lt;/td&gt;
&lt;td&gt;属性&lt;/td&gt;
&lt;td&gt;方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;对于 Java&lt;/td&gt;
&lt;td&gt;实例变量&lt;/td&gt;
&lt;td&gt;函数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;建立以下 两个文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// static方法
public class Dog {
    public static void makeNoise() {
        System.out.println(&quot;Bark!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class DogLauncher {
    public static void main(String[] args) {
        Dog.makeNoise();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，&lt;code&gt;static&lt;/code&gt; 是静态的意思。被它修饰的方法，应该直接用类名调用；而没有 static 的方法，应该先实例化一个类（生成一个对象），用对象名来调用方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 非静态方法，也被称为实例方法
public class Dog {
    public void makeNoise() {
        System.out.println(&quot;Bark!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class DogLauncher {
    public static void main(String[] args) {
    	Dog xibei = new Dog； // 这里实例化出一个xibei对象，类型为Dog。
        xibei.makeNoise();  // 这里通过对象名来调用方法。
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;总结：实例方法和实例变量需要实例化之后，通过对象名来使用；静态方法和静态变量可以直接使用类名来使用。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;构造器（Constructors）&lt;/h2&gt;
&lt;p&gt;实例化一个对象的时候，需要进行一些初始化（例如一个婴儿出生，需要有姓名、性别、体重等信息）。为了初始化这个对象，我们需要写构造器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Dog {
    public int weightInPounds;

	// 构造函数
    public Dog(int w) {
        weightInPounds = w;
    }

    public void makeNoise() {
        if (weightInPounds &amp;lt; 10) {
            System.out.println(&quot;yipyipyip!&quot;);
        } else if (weightInPounds &amp;lt; 30) {
            System.out.println(&quot;bark. bark.&quot;);
        } else {
            System.out.println(&quot;woof!&quot;);
        }    
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数组&lt;/h2&gt;
&lt;p&gt;数组也是需要实例化的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ArrayDemo {
    public static void main(String[] args) {
        /* Create an array of five integers. */
        int[] someArray = new int[5];
        someArray[0] = 3;
        someArray[1] = 4;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类方法与实例方法&lt;/h2&gt;
&lt;p&gt;Java 允许我们定义两种类型的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类方法，又名静态方法。&lt;/li&gt;
&lt;li&gt;实例方法，又称非静态方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;静态变量&lt;/h2&gt;
&lt;p&gt;类具有静态变量有时很有用。这些是类本身固有的属性，而不是实例。&lt;/p&gt;
&lt;h2&gt;Main 函数&lt;/h2&gt;
&lt;p&gt;Main 函数长的像这样：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public static void main(String[] args)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;解释一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public&lt;/code&gt;：到目前为止，我们所有的方法都以这个关键字开头。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static&lt;/code&gt;：它是一个静态方法，不与任何特定实例相关联。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void&lt;/code&gt;：它没有返回类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt;：这是方法的名称。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String[] args&lt;/code&gt;：这是传递给 main 方法的参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;命令行参数&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;String[] args&lt;/code&gt; 意味着可以在命令行中给 main 函数传入多个参数。&lt;/p&gt;
</content:encoded></item><item><title>CS 61B | Hello World Java</title><link>https://mrsibe.top/posts/cs-61b/01-hello-world-java/</link><guid isPermaLink="true">https://mrsibe.top/posts/cs-61b/01-hello-world-java/</guid><description>CS 61B 学习笔记：从 Hello World 和命令行入口开始，熟悉 Java 程序的编译与运行流程，并整理变量、循环、强类型语言和函数定义等基础语法。</description><pubDate>Mon, 03 Nov 2025 02:47:08 GMT</pubDate><content:encoded>&lt;h2&gt;Hello world&lt;/h2&gt;
&lt;p&gt;下面是 Java 的第一个 Hello world。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HelloWorld {
    public static void main(String[] args) {
        System.out.println(&quot;Hello world!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;面向对象的特性是：所有的代码都放在类（class）里面。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public static void main (String[] args)&lt;/code&gt; 是 main函数的定义。&lt;/p&gt;
&lt;h2&gt;Java 编译运行&lt;/h2&gt;
&lt;p&gt;安装好 JDK 之后，想要运行 Java 程序，需要以下两个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编译 Java 源文件。&lt;/li&gt;
&lt;li&gt;运行字节码文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ javac HelloWorld.java  # 1. javac指令编译
$ java HelloWorld        # 2. java指令运行
Hello World! 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;变量和循环&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class HelloNumbers {
    public static void main(String[] args) {
        int x = 0;
        while (x &amp;lt; 10) {
            System.out.print(x + &quot; &quot;);
            x = x + 1;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译运行之后这样显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ javac HelloNumbers.java
$ java HelloNumbers
0 1 2 3 4 5 6 7 8 9 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里主要是区分 &lt;code&gt;System.out.println&lt;/code&gt; 和 &lt;code&gt;System.out.print&lt;/code&gt; 的区别：&lt;code&gt;System.out.println&lt;/code&gt; 会换行， &lt;code&gt;System.out.print&lt;/code&gt; 则不换行。&lt;/p&gt;
&lt;h2&gt;强类型语言&lt;/h2&gt;
&lt;p&gt;Java 是一个&lt;strong&gt;强制类型定义&lt;/strong&gt;的语言（大部分编译型的编程语言都是强类型），也就是说我们定义一个变量的时候需要声明它的&lt;strong&gt;类型&lt;/strong&gt;和名称。&lt;/p&gt;
&lt;p&gt;Python 则相反，x 可以是 int，过一会也可以变成 Dog 类型都可以。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HelloNumbers {
    public static void main(String[] args) {
        int x = 0;
        while (x &amp;lt; 10) {
            System.out.print(x + &quot; &quot;);
            x = x + 1;
        }
        x = &quot;horse&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译后会显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ javac HelloNumbers.java 
HelloNumbers.java:9: error: incompatible types: String cannot be converted to int
        x = &quot;horse&quot;;
                ^
1 error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是强类型定义的证明：定义为整数类型的 &lt;code&gt;x&lt;/code&gt;，在经过 &lt;code&gt;x = &quot;horse&quot;&lt;/code&gt; 赋值语句之后通过不了编译。&lt;/p&gt;
&lt;p&gt;当然有例外：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String h = 5 + &quot;horse&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条语句就可以通过。Java 具有字符串拼接的特性，数字和字符串通过 &lt;code&gt;+&lt;/code&gt; 拼接在一起，数字会强制转换为字符串：&lt;code&gt;&quot;5horse&quot;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Java 函数定义&lt;/h2&gt;
&lt;p&gt;先来一段 Python：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def larger(x, y):
    if x &amp;gt; y:
        return x
    return y

print(larger(8, 10))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;刚刚提到过 Java 的所有代码写在类里面，所以加上类，改一下 print 的方法就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class LargerDemo {
    public static int larger(int x, int y) {
        if (x &amp;gt; y) {
            return x;
        }
        return y;
    }

    public static void main(String[] args) {
        System.out.println(larger(8, 10));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码规范&lt;/h2&gt;
&lt;p&gt;详细的代码规范可以在这里查看：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://sp19.datastructur.es/materials/guides/style-guide.html&quot;&gt;Style Guide | CS 61B Spring 2019&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;顺便提一下 Javadoc：有一种注释方法，可以通过 Javadoc 生成 HTML 文档。Java 程序员可以给每一个函数加上这样的注释，用 Javadoc 生成 HTML，方便后续的程序员读懂代码。&lt;/p&gt;
&lt;p&gt;这种注释方法是：&lt;code&gt;/** 注释内容 */&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>Hot 100 | 1. 两数之和</title><link>https://mrsibe.top/posts/hot-100/1-two-sum/</link><guid isPermaLink="true">https://mrsibe.top/posts/hot-100/1-two-sum/</guid><description>LeetCode Hot 100 第一题两数之和的完整题解：从暴力双重循环、排序加双指针到哈希表三种解法逐步优化，并说明哈希表为什么能把查找过程降到线性时间。</description><pubDate>Sat, 25 Oct 2025 06:20:20 GMT</pubDate><content:encoded>&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;给定一个整数数组 &lt;code&gt;nums&lt;/code&gt; 和一个整数目标值 &lt;code&gt;target&lt;/code&gt;，请你在该数组中找出 &lt;strong&gt;和为目标值&lt;/strong&gt; &lt;em&gt;&lt;code&gt;target&lt;/code&gt;&lt;/em&gt;  的那 &lt;strong&gt;两个&lt;/strong&gt; 整数，并返回它们的数组下标。&lt;/p&gt;
&lt;p&gt;你可以假设每种输入只会对应一个答案，并且你不能使用两次相同的元素。&lt;/p&gt;
&lt;p&gt;你可以按任意顺序返回答案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输入：&lt;/strong&gt; nums = [2,7,11,15], target = 9
&lt;strong&gt;输出：&lt;/strong&gt;[0,1]
&lt;strong&gt;解释：&lt;/strong&gt; 因为 nums[0] + nums[1] == 9 ，返回 [0, 1] 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输入：&lt;/strong&gt; nums = [3,2,4], target = 6
&lt;strong&gt;输出：&lt;/strong&gt;[1,2]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输入：&lt;/strong&gt; nums = [3,3], target = 6
&lt;strong&gt;输出：&lt;/strong&gt;[0,1]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;2 &amp;lt;= nums.length &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-109 &amp;lt;= nums[i] &amp;lt;= 109&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-109 &amp;lt;= target &amp;lt;= 109&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只会存在一个有效答案&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;进阶：&lt;/strong&gt; 你可以想出一个时间复杂度小于 &lt;code&gt;O(n2)&lt;/code&gt; 的算法吗？&lt;/p&gt;
&lt;h2&gt;思路&lt;/h2&gt;
&lt;h3&gt;思路一：暴力双重循环&lt;/h3&gt;
&lt;p&gt;直接遍历完所有的可能性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {  
    public int[] twoSum(int[] nums, int target) {  
        for (int i = 0; i &amp;lt; nums.length; i++) {  
            for (int j = i + 1; j &amp;lt; nums.length; j++) {  
                if (nums[i] + nums[j] == target) {  
                    return new int[]{i, j};  
                }  
            }  
        }  
        return new int[]{};  
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度高达 O(N^2)。&lt;/p&gt;
&lt;h3&gt;思路二：双指针&lt;/h3&gt;
&lt;p&gt;首先用快排排序，O(NlogN)；然后用双指针往中间移动：如果指针所指元素之和小于 &lt;code&gt;target&lt;/code&gt; ，左指针右移，这样两个元素之和就可以变大；相反，如果指针所指元素之和大于 &lt;code&gt;target&lt;/code&gt; ，右指针左移，这样两个元素之和就可以变小。&lt;/p&gt;
&lt;p&gt;这样的做法比较麻烦的是需要自己写排序算法：因为最终程序输出的是索引，排序会把索引打乱，所以我们需要专门写一个索引数组，进行被排序数组同样的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {  
    public int[] twoSum(int[] nums, int target) {
    	// 创建索引数组
        int[] index = new int[nums.length];  
        for (int i = 0; i &amp;lt; nums.length; i++) {  
            index[i] = i;  
        }  
        
        // 排序
        Arrays.sort(nums);
        
        // 双指针
        int leftPointer = 0, rightPointer = nums.length - 1;  
        while (leftPointer &amp;lt; rightPointer) {  
        	// 如果和等于 target 则返回
            if (nums[leftPointer] + nums[rightPointer] == target) {  
                int left = index[leftPointer];  
                int right = index[rightPointer];  
                if (left &amp;lt; right) {  
                    return new int[]{left, right};  
                } else {  
                    return new int[]{right, left};  
                }  
            } else if (nums[leftPointer] + nums[rightPointer] &amp;lt; target) {  
                leftPointer++;  
            } else {  
                rightPointer--;  
            }  
        }
        // 没找到返回空数组
        return new int[]{};  
    }  
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思路三：哈希表（最常用）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Solution {  
    public int[] twoSum(int[] nums, int target) {
    	Map&amp;lt;Integer, Integer&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
    	for (int i = 0; i &amp;lt; nums.length; i++) {
    		if (map.containsKey(target - nums[i])) {
    			return new int[]{map.get(target - nums[i]), i};
    		}
    		map.put(nums[i], i);
    	}
        // 没找到返回空数组
        return new int[]{};  
    }  
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>CS 61B | 导论</title><link>https://mrsibe.top/posts/cs-61b/00-intro/</link><guid isPermaLink="true">https://mrsibe.top/posts/cs-61b/00-intro/</guid><description>开始学习 Berkeley CS 61B 时的准备记录：整理课程定位、推荐的学习方法与节奏、常用学习资源，以及本地 Java 开发环境的搭建过程。</description><pubDate>Fri, 24 Oct 2025 07:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;与我们平时枯燥乏味的教育风格不同，美国的计算机教育似乎更为风趣。计算机四大名校（Stanford, CMU, MIT, Berkeley）还公开了一系列基础的计算机课程，这些课程以极高的教育质量著称，并通过爆火的 CSDIY 等计算机自学网站广为人知。&lt;/p&gt;
&lt;p&gt;因此西贝打算驻足一下，去体验一下这些国外计算机课程。&lt;/p&gt;
&lt;p&gt;最首要的问题是，&lt;strong&gt;我应该刷哪些课程&lt;/strong&gt;？这个问题已经在下面这个 course guide 得到解答：&lt;a href=&quot;https://hkn.eecs.berkeley.edu/courseguides&quot;&gt;Eta Kappa Nu (HKN), Mu Chapter&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个 course guide 是 Berkeley 推出的，因此里面都是 Berkeley 的课程，可能具有局限性。但是其中的核心课程：CS 61A、CS 61B 和 CS 61C，非常值得学习。尤其是 CS 61A，作为计算机经典书籍 SICP 的改编，可以说是 CS 课程系列的精华之作，非常值得一学。&lt;/p&gt;
&lt;h2&gt;CS 61B is all you need.&lt;/h2&gt;
&lt;p&gt;CS 61B 是一门用 Java 学习的&lt;strong&gt;算法与数据结构&lt;/strong&gt;课程（重点在数据结构和算法，而不是 Java！）。&lt;/p&gt;
&lt;p&gt;CS 61B 已经开展多年，每年都有大量的课件和资料流传下来。我以资源的完整性，选择 &lt;strong&gt;sp21 版本&lt;/strong&gt;的 CS 61B 进行学习。&lt;/p&gt;
&lt;h3&gt;学习方法&lt;/h3&gt;
&lt;p&gt;我计划的学习方式：&lt;strong&gt;Lecture -&amp;gt; Reading -&amp;gt; Discussion -&amp;gt; Lab -&amp;gt; HW -&amp;gt; Project&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;整个流程是围绕 CS 61B 的官方网站进行的（文章最后会附有官网链接）。&lt;/p&gt;
&lt;p&gt;课程的 Reading 和 Lecture video 是配套的，因此我打算首先观看B站上的 Lecture，然后一边阅读 Reading 一边记好笔记，然后看一下 Discussion 的材料。我可以在上午完成视频观看，下午阅读 Reading 做笔记。&lt;/p&gt;
&lt;p&gt;与国内传统计算机教育不同，Berkeley 的计算机课程非常注重实践，因此 Lab 和 HW 是重头戏。据刷课的佬们的建议，我打算把 Lab, HW 和 Project 放在周末，一方面是平时碎片化时间容易打断思路，另一方面，Lab 可能涉及下一周的 Lecture 知识，需要加快视频学习。&lt;/p&gt;
&lt;p&gt;还有，CS 61B 的 Lab 和 HW 支持在线评测，只需要在 Gradescope 平台上上传代码压缩包或者自己的 GitHub 仓库即可（太酷啦！）。&lt;/p&gt;
&lt;h3&gt;学习资源&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;官方网站：&lt;a href=&quot;https://sp21.datastructur.es/&quot;&gt;Main | CS 61B Spring 2021&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Gradescope 评测平台：&lt;a href=&quot;https://www.gradescope.com/&quot;&gt;Gradescope&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;官方 GitHub 代码仓库：&lt;a href=&quot;https://github.com/Berkeley-CS61B/lectureCode-sp21&quot;&gt;GitHub - Berkeley-CS61B/lectureCode-sp21&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>CS 168 | 导论</title><link>https://mrsibe.top/posts/cs-168/introduction/</link><guid isPermaLink="true">https://mrsibe.top/posts/cs-168/introduction/</guid><description>我的 Berkeley CS 168 计算机网络学习笔记开篇：从互联网的基本结构讲起，梳理分层模型、协议、报头与端到端原则，帮助理解网络设计背后的取舍。</description><pubDate>Tue, 07 Oct 2025 13:31:58 GMT</pubDate><content:encoded>&lt;h2&gt;一、互联网导论&lt;/h2&gt;
&lt;h3&gt;互联网是什么？&lt;/h3&gt;
&lt;p&gt;互联网不是我们平时打开浏览器看到的花花绿绿的网页，那只是互联网应用的一部分——万维网。电子邮件、我的世界游戏服务器，甚至物联网设备都算互联网的一份子。&lt;/p&gt;
&lt;h3&gt;互联网是联盟式的、可扩展的&lt;/h3&gt;
&lt;p&gt;互联网由很多运营商（ISP）在维护，每个运营商独立运作，但每个运营商都必须合作以连接整个世界。因此运营商们必须达成一致的协议，才能让互联网遍布世界的每一个角落。&lt;/p&gt;
&lt;p&gt;互联网不是“金字塔层级”，也就是不是只有一个运营商，因此我们互联网需要做的就是把各个运营商链接在一起，每个局域网连接在一起，每个设备连接在一起... 这就是互联网的可扩展性。&lt;/p&gt;
&lt;p&gt;庞大体积也给互联网本身带来很多挑战，比如消息可能在传输中途就失效了，再比如中途有一个或者很多个路由器坏了等等，因此互联网需要面对随机和众多的故障。&lt;/p&gt;
&lt;h3&gt;协议&lt;/h3&gt;
&lt;p&gt;互联网的重点在于协议。协议就是一种标准，规定了互联网中的数据以什么方式、格式传输，从哪传送到哪，这样才能保证一致性。协议一般是以请求评论（RFC）的方式发布的，由 IETF 负责。&lt;/p&gt;
&lt;h2&gt;二、互联网层次&lt;/h2&gt;
&lt;h3&gt;第一层：物理层&lt;/h3&gt;
&lt;p&gt;物理层的作用就是把&lt;strong&gt;位信息&lt;/strong&gt;通过某种方式传输出去。这种方式通常有电线上的电压、无线无线电波、光纤电缆中的光脉冲等，这是电气领域的事情，是计算机网络的最低层次，我们不作深究。&lt;/p&gt;
&lt;h3&gt;第二层：数据链路层&lt;/h3&gt;
&lt;p&gt;在互联网中，一条链路连接两台机器，多条链路连接多台机器构成了&lt;strong&gt;局域网（local area network，LAN)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;链路层主要是把物理层的位信息分组成&lt;strong&gt;数据包（packets）&lt;/strong&gt;（有时候也叫帧frames），以其作为基本单位。&lt;/p&gt;
&lt;h3&gt;第三层：网络层&lt;/h3&gt;
&lt;p&gt;不同局域网的设备想要沟通，就必须搭建局域网之间的桥梁，不同的局域网连接起来就构成了互联网。这个桥梁的两个端点就是&lt;strong&gt;交换器（switch）&lt;/strong&gt; 或者 &lt;strong&gt;路由器（router）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;网络层解决了以下问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;交换机或者路由器收到数据包，该往哪里转发？怎么发路径最短？（路由单元的重点）&lt;/li&gt;
&lt;li&gt;如何保证链路上有足够的容量来传输我们的数据？（拥塞控制单元的重点）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;网络层的两个特性是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;尽力而为传输：只管传输，不管有没有传输到目的地，尽力就好。&lt;/li&gt;
&lt;li&gt;数据包抽象：如果传输大量数据，就把它切分成多个小数据包传输。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;第四层：传输层&lt;/h3&gt;
&lt;p&gt;传输层以第三层网络层为基础，实现了一个额外的&lt;strong&gt;协议&lt;/strong&gt;，用于重传丢失的数据包、将数据分割成数据包，以及重新排序乱序到达的数据包（以及其他功能）。&lt;/p&gt;
&lt;p&gt;传输层协议使我们能够停止以数据包为单位思考，转而以&lt;strong&gt;流&lt;/strong&gt;的形式思考，即两个端点之间交换的&lt;strong&gt;数据包流&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;第七层：应用层&lt;/h3&gt;
&lt;p&gt;应用层使得互联网可以支持不同的应用，例如收邮件，下载视频等等。如果没有第七层，恐怕互联网就只能有一种用途，换句话说应用层使得互联网有更多的应用。&lt;/p&gt;
&lt;p&gt;会话层（第五层）原本应该将不同的数据流组装成一个会话（例如，加载各种图像和广告来形成一个网页），而表示层（第六层）原本应该帮助用户可视化数据。如今，这些层的功能主要在第七层中实现，因此直接被跳过了。&lt;/p&gt;
&lt;h2&gt;三、报头&lt;/h2&gt;
&lt;h3&gt;为什么需要报头&lt;/h3&gt;
&lt;p&gt;在第三层网络层我们提出了一个问题：数据包应该经过什么路径发送？&lt;/p&gt;
&lt;p&gt;这件事关乎交换机或者路由器，我们应该提前告诉他们这个数据包往哪发送。实现方法就是在数据前面加上&lt;strong&gt;报头（headers）&lt;/strong&gt;。而数据在这里我们就叫它&lt;strong&gt;载荷（payload）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;每一个网络设备都需要遵循一个报头标准，就像世界通用一个语言——英语，这样数据包的传输才不会乱套。&lt;/p&gt;
&lt;h3&gt;报头里有什么信息&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;目标地址：告诉路由器和交换器，数据包要往何处发送。&lt;/li&gt;
&lt;li&gt;源地址：允许数据接收方可以回信。&lt;/li&gt;
&lt;li&gt;校验和：确保数据包在传输中途没有损坏。&lt;/li&gt;
&lt;li&gt;其他元数据：例如数据包的长度。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;报头疯狂嵌套&lt;/h3&gt;
&lt;p&gt;信息顺着互联网层次越低，修饰的报头就越多；层次越高，报头就越少。&lt;/p&gt;
&lt;p&gt;每一个设备都会处理前三层：物理层、数据链路层和网络层，但只有发信和收信的主机才可以处理后两层：传输层和应用层。经过这样不停的拆包和封装过程，数据最终被传输到目的地址。&lt;/p&gt;
&lt;p&gt;第一层、第二层的协议可以采用无线或者有线方式传输，但是必须和上一跳或者下一跳的协议一样；第四层和第七层中，发送主机和目标主机的协议必须完全一样，以保证功能相同。&lt;/p&gt;
&lt;h2&gt;三、网络架构&lt;/h2&gt;
&lt;h3&gt;设计范式&lt;/h3&gt;
&lt;p&gt;上一节我们自底向上了解了互联网层次，现在我们自顶向下来认识互联网设计采用的范式。&lt;/p&gt;
&lt;p&gt;互联网的范式有很多也很有争议，比如联盟式（独立运营商合作），但在近年来，软件定义网络（SDN）作为一种更集中的网络管理方式出现。&lt;/p&gt;
&lt;h3&gt;“楚王好细腰”&lt;/h3&gt;
&lt;p&gt;互联网层次中的一层可以只有一个协议，也可以有很多很多协议。&lt;/p&gt;
&lt;p&gt;现代互联网的具体协议如上图所示，形成了一个上下宽中间窄的图像。可以发现在第三层只有 IP 协议，互联网的每一个人都必须使用 IP 协议才能传输数据。&lt;/p&gt;
&lt;h3&gt;解复用&lt;/h3&gt;
&lt;p&gt;上面说到，多个协议可能采用同一个路径传递，因此在第四层解包的时候我们要在报头中声明，下一个协议具体是什么协议（TCP/UDP）；而在第七层我们需要声明下一个端口是什么。端口用来区分数据包属于哪个应用程序。&lt;/p&gt;
&lt;p&gt;端口分为逻辑端口和物理端口。逻辑端口就是报头里面的 port 字段；物理端口就是交换机上插的网线物理位置。&lt;/p&gt;
&lt;p&gt;注意：套接字（socket）指的是操作系统用于将应用程序连接到操作系统中的网络栈的机制。当应用程序打开一个套接字时，该套接字将与一个逻辑端口号相关联。当操作系统接收到一个数据包时，它使用端口号将该数据包定向到相关联的套接字。&lt;/p&gt;
&lt;h3&gt;端到端原则&lt;/h3&gt;
&lt;p&gt;端到端原则，说白了就是让互联网的底层专注于数据传输，而高层主要作服务。这个思想为设计互联网提供了智慧和指导，指导着关于网络实现哪些功能、不实现哪些功能的讨论。这里具体不细究。&lt;/p&gt;
&lt;h2&gt;四、资源共享设计&lt;/h2&gt;
&lt;h3&gt;统计复用&lt;/h3&gt;
&lt;p&gt;之前在第三层提出了第二个问题：交换机和路由器容量有限，如何在不同互联网用户之间共享这些资源？&lt;/p&gt;
&lt;p&gt;答案是：网络资源是&lt;strong&gt;统计复用&lt;/strong&gt;的，这意味着我们会&lt;strong&gt;根据用户的需求动态分配资源&lt;/strong&gt;，而不是将固定份额的资源分配给用户。&lt;/p&gt;
&lt;p&gt;统计复用能够工作的前提是：&lt;strong&gt;在实践中，总需求的峰值远小于各峰值需求的总和。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;统计复用是一种具有权衡的设计选择，我们不会为最坏情况（所有需求同时达到峰值）进行配置。峰值仍可能同时发生，这将导致数据包延迟或丢失。&lt;/p&gt;
&lt;h3&gt;电路交换 VS 分组交换&lt;/h3&gt;
&lt;p&gt;我们可以使用&lt;strong&gt;统计复用&lt;/strong&gt;来决定建设多少容量。我们的下一个问题是：我们实际上是如何在用户之间动态分配资源的？&lt;/p&gt;
&lt;p&gt;网络共享资源的方式有两种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;尽力而为&lt;/strong&gt;：数据包发出去就不管了。尽力而为的最佳设计称为&lt;strong&gt;分组交换&lt;/strong&gt;：交换机独立地查看每个分组，并将分组转发到离目的地更近的地方。交换机不会考虑流或预留。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预留&lt;/strong&gt;：在流开始时，用户明确请求并预留所需的带宽。数据发送完毕后，资源可以被释放供其他人预留。预留的最佳设计，称为&lt;strong&gt;电路交换&lt;/strong&gt;。两台主机直接搭建好数据传递的通道之后再进行传输。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;电路交换和分组交换都体现了统计复用。主要区别在于我们分配资源的粒度：&lt;strong&gt;预留按流分配，尽力而为按包分配。&lt;/strong&gt; 即使在电路交换中，我们也是根据预留动态分配资源。我们不是预先为所有可能存在的流进行预留。&lt;/p&gt;
&lt;h3&gt;电路交换与分组交换的权衡&lt;/h3&gt;
&lt;p&gt;为了比较两者，我们作四个维度的比较：&lt;/p&gt;
&lt;h4&gt;1. 能否提供给应用程序开发者的抽象 ？&lt;/h4&gt;
&lt;p&gt;电路交换为开发者提供了更有用的抽象，因为可以保证预留的带宽，网络的行为可以预测。对于网络运营商来说，更方便统计流量，方便收费。&lt;/p&gt;
&lt;h4&gt;2. 资源利用效率如何？&lt;/h4&gt;
&lt;p&gt;分组交换通常更高效。如果每个发送者在整个时间段内以恒定速率发送数据，那么电路交换和分组交换都能充分利用容量；如果每个发送者的速率随时间变化，那么分组交换能更好地利用带宽。&lt;/p&gt;
&lt;p&gt;一个数据流的突发性是由其峰值速率与平均速率的比值定义的。语音通话通常具有较平滑的比值，如 3:1，而网页浏览通常具有较突发的比值，如 100:1。&lt;/p&gt;
&lt;p&gt;分组交换更高效的原因之二是：电路交换在建立和拆除电路时需要额外的时间。这对于非常短的数据尤其低效。&lt;/p&gt;
&lt;h4&gt;3. 处理故障的效果如何？&lt;/h4&gt;
&lt;p&gt;分组交换在规模上处理故障方面表现更好。如果路由器发生故障，我们可以将数据包沿网络中的其他路径发送。&lt;/p&gt;
&lt;h4&gt;4. 实现有多复杂？&lt;/h4&gt;
&lt;p&gt;状态一致性问题导致电路交换非常复杂。所有路由器要保证同时接通，都必须跟踪额外的状态并保持一致。&lt;/p&gt;
&lt;p&gt;总之：电路交换通过预留带宽为应用提供更好的性能，同时也为开发者提供更可预测的行为。分组交换能更有效地共享带宽，并避免启动时间，更容易从故障中恢复，更易于实现。&lt;/p&gt;
&lt;h3&gt;在现实中...&lt;/h3&gt;
&lt;p&gt;分组交换是默认的资源共享方法，但是在专用网络中电路交换用的比较多。&lt;/p&gt;
&lt;h2&gt;五、链接&lt;/h2&gt;
&lt;h3&gt;链接的属性&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;带宽（bandwidth）：告诉我们每单位时间内可以在链路上发送多少比特，单位通常用比特每秒（bps）。&lt;/li&gt;
&lt;li&gt;传播延迟（propagation delay）：一个比特沿链路传输需要多长时间。&lt;/li&gt;
&lt;li&gt;带宽延迟积（BDP）：带宽和传播延迟的乘积，直观上这是链路的容量。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;时序图&lt;/h3&gt;
&lt;p&gt;假设我们有一条带宽为 1 Mbps = 每秒 100 万比特，传播延迟为 1 ms = 0.001 秒的链路。发送一个 bit 需要 0.000001 秒，时序图如下：&lt;/p&gt;
&lt;p&gt;假如发送 800 个 bits，时序图如下：&lt;/p&gt;
&lt;h3&gt;数据包延迟&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;数据包延迟&lt;/strong&gt;是指整个数据包从第一个比特被放到线路上开始，到最后一个比特在另一端被接收为止所需要的时间。这种延迟是传输延迟和传播延迟的总和。&lt;/p&gt;
&lt;p&gt;传输延迟告诉我们将比特放到线路上需要多长时间，通常是数据包大小除以链路带宽。&lt;/p&gt;
&lt;p&gt;我们权衡带宽和传播延迟主要看数据包延迟。带宽越大，传输延迟越小。&lt;/p&gt;
&lt;h3&gt;管道图&lt;/h3&gt;
&lt;p&gt;还有一种管道图也可以表示链路：&lt;/p&gt;
&lt;h3&gt;重载链接&lt;/h3&gt;
&lt;p&gt;当同时有两个数据包到达路由器，其中一个数据包会通过路由器处理，另外一个则放在路由器的队列里面，这也会导致延迟。因此，&lt;strong&gt;数据包延迟是传输延迟、传播延迟和排队延迟的总和&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>我用过 WordPress、VuePress、Hexo 和 Hugo 后的选择</title><link>https://mrsibe.top/posts/choosing-a-blog-framework/</link><guid isPermaLink="true">https://mrsibe.top/posts/choosing-a-blog-framework/</guid><description>回顾我用过的 WordPress、VuePress、Hexo 和 Hugo 四个博客框架，比较它们在易用性、可定制性与部署方式上的差异，并说明我最终为什么把博客迁移到 Hugo。</description><pubDate>Mon, 29 Sep 2025 03:11:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;更新于 2026-08-29：本文写于我使用 Hexo + Fluid 的阶段。目前博客已经迁移到 Hugo + PaperMod，原文保留作为当时的使用记录，文末补充了现在的选择。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;瞎折腾是我一直以来的主旋律，光是网站我就折腾了不少框架。今天给大伙们分享一下自己曾经用过的一些框架。&lt;/p&gt;
&lt;h2&gt;WordPress：小白建站不二选择&lt;/h2&gt;
&lt;p&gt;计算机领域有个词：低代码平台，意思大概就是几乎不用写代码，就可以实现自己想要的功能。WordPress 就是这样的，你不用写一行代码就可以快速搭建起一个像模像样的网站。&lt;/p&gt;
&lt;p&gt;WordPress 有插件市场，还自带了海量企业级、博客级的模板，真的就像模像样...&lt;/p&gt;
&lt;p&gt;我接触的第一个博客框架就是 WordPress。当时是想自己建立一个天文文章汇总网站，任何天文爱好者都可以在网站上发布文章（现在想起来有点幼稚，但是也很有意思）。当时还没有大模型，我在必应上面搜了一整天博客框架，几乎百分之 80 的回答都是 WordPress。于是我自己买了一个阿里云服务器，跌跌撞撞花了一整个月备案，选了一个自以为好看的主题 Bravada，搭建了人生中第一个网站——银河派对。&lt;/p&gt;
&lt;p&gt;当时实在是乐在其中，而且那个时候很多天文大佬发文都很精华，属于是赶上浪潮之巅了。更可贵的是凭着厚脸皮联系上了不少大佬，后面打造自己的社群。高中的这番经历让我对建站有了大概的了解，这段经历也很可能潜移默化地影响了我未来走计算机的道路。&lt;/p&gt;
&lt;p&gt;最后总结一下：如果你不太熟悉写代码，WordPress 是最佳选择。&lt;/p&gt;
&lt;h2&gt;VuePress：高度自定义友好&lt;/h2&gt;
&lt;p&gt;银河派对最后是因为没有能力续费阿里云 ECS 和域名，被迫关停了。不过到了大学我有了建立自己博客网站的需求。我对于博客网站的要求只有两个：一个是优雅，简洁而不简陋；另一个是可以用 markdown 格式写文章。WordPress 的模板大多花里胡哨、加载慢，很难达到第一个需求。通过大量调研，我发现自己看过的不少网站都是一种风格，似乎是同一套框架写出来的，最后我发现了VuePress。&lt;/p&gt;
&lt;p&gt;VuePress 做出来的网站差不多都这个样子，网上称呼这种风格为“文档风”（看上去就像代码文档一样，不过确实很多文档也就用的 VuePress）。同类型的博客框架还有 VitePress，也是这种风格。&lt;/p&gt;
&lt;p&gt;于是我把高中那一套用了起来：买 ECS、备案... 后面的就几乎不会了：什么 git 克隆仓库，什么 vue 组件，后面我连文章写在哪，怎么运行网站都完全不知道！因为这类框架需要一定的计算机基础，适合计算机从业人群使用。我又花了一个月研究 github、简单入门 vue 和 vite，边看 VuePress 的官方文档边修改网站，这才上线了第二个网站——西贝的天文茶馆。&lt;/p&gt;
&lt;p&gt;VuePress 是我目前用过的最久的框架，因为它满足了我对网站风格的基本需求，markdown 写文章也相当方便，可以迁移到微信公众号、知乎等平台，而且 VuePress 是静态网站，加载速度也相对 WordPress 花里胡哨的动画快不少。&lt;/p&gt;
&lt;p&gt;总的来说，如果你懂一些计算机知识，喜欢简洁优雅的主题，VuePress/VitePress 是可以考虑的框架。&lt;/p&gt;
&lt;h2&gt;Hexo：生态圈完整的程序员友好框架&lt;/h2&gt;
&lt;p&gt;最近我实在有点不想忍受 VuePress 的一大槽点：文章和配置文件不分离。写文章需要在项目深处精确地创建 markdown 文件，并且旁边一大堆配置文件看着很乱。于是我又增添了一个需求：文章与配置文件分离，项目短小精悍。&lt;/p&gt;
&lt;p&gt;这次我交给大模型来调研，大模型给我推荐了两个框架：Hugo 和 Hexo。Hexo 是基于 Node.js 构建的，Hugo 是基于 Go 语言构建的。Hexo 很多程序员都在用作个人博客，有许多插件和主题，这跟 WordPress 很像；Hugo 构建速度快，适合高度自定义。我不愿意再花太多心思折腾网站布局，于是我选择了 Hexo 来快速部署一个新的博客网站。&lt;/p&gt;
&lt;p&gt;主题选什么好？Hexo 目前最热门的框架是 NexT 主题，大概长这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/16272760/63487983-da41b080-c4df-11e9-951c-64883a8a5e9b.png&quot; alt=&quot;NexT主题样式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最下面这个主题相信不少人见过，非常经典！但是这个主题我不认为太简洁，这太简陋了。我相信程序员很难接受大面积的方块与尖锐的边缘。于是我开始寻找自己想要风格：Material Design。&lt;/p&gt;
&lt;p&gt;Material Design 很重视组件及其层级关系。我觉得它的特点是：组件简洁，只用圆角修饰；组件有一定遮挡关系，杜绝完全的平铺。这很合我的胃口，于是我选择了 Material Design 的 Hexo 主题：fluid。（有一说一其实 VuePress 也像是 Material Design）&lt;/p&gt;
&lt;p&gt;下载好主题，花半个小时简单配置一下就能用，现在 hexo + fluid 已然变成了我的心头爱。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;如果你打算自己建立一个网站，且没有计算机基础，毫无疑问直接选择 WordPress。&lt;/p&gt;
&lt;p&gt;如果你有计算机基础，并且想要类似于文档风的组织，可以选择 VuePress/VitePress，否则 Hexo。当然如果不想思考太多可以直接选择 Hexo，因为它的生态、解决方案比 VuePress 多不少。&lt;/p&gt;
&lt;p&gt;运行网站不一定要自己维护服务器。静态博客可以部署到 GitHub Pages、Cloudflare Pages 等平台，通常能够接入 Git 自动构建；也可以购买云服务器自行部署，换取更多控制权。&lt;/p&gt;
&lt;h2&gt;现在为什么使用 Hugo&lt;/h2&gt;
&lt;p&gt;继续使用一段时间后，我还是希望博客仓库更简单、构建更快，也希望能直接控制 Markdown、模板和部署流程。Hugo 的单文件程序、构建速度和内容组织方式更符合我现在的习惯，所以最终把博客迁移到了 Hugo，并使用 PaperMod 作为尽量克制的主题基线。&lt;/p&gt;
&lt;p&gt;这并不意味着 Hugo 对所有人都更好。上面的比较更多是在记录我的需求怎样变化，而不是给出一个通用排名。&lt;/p&gt;
</content:encoded></item><item><title>MIT 6.S081 | Lab util</title><link>https://mrsibe.top/posts/mit-6s081/lab-util/</link><guid isPermaLink="true">https://mrsibe.top/posts/mit-6s081/lab-util/</guid><description>我完成 MIT 6.S081 Lab util 时的实现记录：围绕 sleep 和 pingpong 两个练习，梳理系统调用、进程与管道的工作方式，并给出具体的解题思路和代码。</description><pubDate>Sun, 28 Sep 2025 10:02:33 GMT</pubDate><content:encoded>&lt;h2&gt;Sleep&lt;/h2&gt;
&lt;p&gt;Sleep 就是完成一个工具：&lt;code&gt;sleep [time]&lt;/code&gt;，输入后休眠 &lt;code&gt;time&lt;/code&gt; 秒。&lt;/p&gt;
&lt;p&gt;完成这个工具需要掌握两个关键点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如何在 shell 中给程序传递参数&lt;/li&gt;
&lt;li&gt;Sleep 的原理&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Main 函数传参&lt;/h3&gt;
&lt;p&gt;C 语言中，Main 函数要么传递参数，要么传递参数。下面的示例写的很清楚。第一种不传递参数，直接在参数列表写一个 void ，不写也是可以的；第二种传递参数看起来就比较迷惑了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(void) {
   // 无参数形式
   return 0;
}
int main(int argc, char *argv[]) {
   // 带参数形式
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;int argc, char *argv[]&lt;/code&gt;，这到底传递的是什么？Argc 其实是参数的个数，agrv 则是参数的指针。也就是说我们 main 函数需要一个大小为 &lt;code&gt;agrc&lt;/code&gt; 的列表，名字叫 &lt;code&gt;argv&lt;/code&gt;，范围是 &lt;code&gt;0-agrc-1&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;比如说我们执行 &lt;code&gt;cp demo.c demo.cpp&lt;/code&gt;，&lt;code&gt;agrc&lt;/code&gt; 为 3，&lt;code&gt;argv&lt;/code&gt; 为  &lt;code&gt;[&apos;cp&apos;, &apos;demo.c&apos;, &apos;demo.cpp&apos;]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;那么 &lt;code&gt;argc&lt;/code&gt; 和  &lt;code&gt;argv&lt;/code&gt; 是如何得出的？这是 shell 的解析器在负责，不需要我们一个一个数参数到底有多少个。&lt;/p&gt;
&lt;p&gt;总结一下：我们输入的指令，会被 shell 解析成 &lt;code&gt;argc&lt;/code&gt; 和  &lt;code&gt;argv&lt;/code&gt; ，然后交给 main 函数来使用。这里面还有更具体的流程，后面再说。&lt;/p&gt;
&lt;h3&gt;Sleep 的原理&lt;/h3&gt;
&lt;p&gt;实验讲义里提示说让我们看看 &lt;code&gt;sysproc.c&lt;/code&gt; 和 &lt;code&gt;user.h&lt;/code&gt;。这就是实现 &lt;code&gt;sleep&lt;/code&gt; 底层的代码。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;user.h&lt;/code&gt; 提到了 sleep 系统调用的定义：&lt;code&gt;int sleep(int);&lt;/code&gt;。传递一个整型，表示要休眠的时钟滴答数；返回一个整型返回值，返回 0 表示正常运行，返回 -1 表示非正常运行。这个 sleep 系统调用则是包装了下面的 &lt;code&gt;sys_sleep&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sysproc.c&lt;/code&gt; 描述了 &lt;code&gt;sys_sleep&lt;/code&gt; 实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uint64 sys_sleep(void)
{
	int n;
	uint ticks0;
	// 参数不对，返回 -1 直接退出
	if(argint(0, &amp;amp;n) &amp;lt; 0) {
		return -1;
	}
	// 读写全局变量 ticks 前加锁，防止并发。
	acquire(&amp;amp;tickslock);
	// 通过读取 ticks 全局变量，ticks0 变量存储当前时间
	ticks0 = ticks;
	// 不停地检查时间，超过要求的时间就退出循环，这叫*忙等*
	while(ticks - ticks0 &amp;lt; n) {
		// 一旦中断，释放锁退出，返回 -1
		if(myproc()-&amp;gt;killed) {
			release(&amp;amp;tickslock);
			return -1;
		}
		// sleep函数，这个函数可以让程序让出锁，避免占用资源
		sleep(&amp;amp;ticks, &amp;amp;tickslock);
	}
	// 退出循环，释放锁，返回0退出
	release(&amp;amp;tickslock);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个系统调用有个好处：忙等检查和中间的让出机制，避免了程序持续对系统资源占用。想一想一个人在图书馆睡大觉，其他人还找不到图书馆的位置，这样是不是占用图书馆的资源呢？&lt;/p&gt;
&lt;h3&gt;解法&lt;/h3&gt;
&lt;p&gt;现在知道了程序如何传参，以及系统调用 sleep 是如何实现的，现在我们就动手来写自己的 &lt;code&gt;sleep&lt;/code&gt; 实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;kernel/types.h&quot;
#include &quot;user/user.h&quot;

int main(int argc, char *argv[]) {
    int n;
    
    // 判断参数个数是否正确
    if(argc != 2) {
        fprintf(2, &quot;usage: sleep &amp;lt;ticks&amp;gt;\n&quot;);
        exit(1);
    }

    // ticks 参数转换为整型
    n = atoi(argv[1]);
    
    // 判断 ticks 是否为正数
    if(n &amp;lt;= 0) {
        fprintf(2, &quot;sleep: ticks must be positive\n&quot;);
        exit(1);
    }
    
    // sleep，发现返回值不对也需要报错
    if(sleep(n) != 0) {
        fprintf(2, &quot;sleep: interrupted\n&quot;);
        exit(1);
    }
    exit(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;网上的解答非常多，都可以多多参照。我的解法重点在于异常的处理。&lt;/p&gt;
&lt;h2&gt;pingpong&lt;/h2&gt;
&lt;p&gt;第二个工具是 &lt;code&gt;pingpong&lt;/code&gt;。它的功能简单来说就是创建两个进程通过两个管道传递信息。先不关具体我们怎么做，首先我们应当搞清楚：什么是进程？什么是管道？怎么去使用？&lt;/p&gt;
&lt;h3&gt;进程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;进程就是正在运行的程序。&lt;/strong&gt; 换句话说，你写的的代码是一个死气沉沉的文本文件，没有任何作用，只有让这个程序在计算机上运行起来，才能发挥作用。程序被计算机加载并运行，就成了进程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作系统调度进程。&lt;/strong&gt; 电脑上时时刻刻都有数不清的进程在运行，但是 CPU 通常只有一个。这很像是小学经典奥数题：把水烧开需要 3 分钟，洗衣服需要 20 分钟，买蛋糕需要 5 分钟... 如何安排才能让自己完成这些事情的时间最少？计算机就面临这个问题，它要用有限的资源高效地做事情，就需要一个东西来调度进程执行的顺序与时间：什么时候运行这个进程，什么时候应该运行那个进程。这个活最开始是由人自己来做的，后来人太懒了，于是发明了操作系统，让操作系统帮忙调度进程的运行。&lt;/p&gt;
&lt;p&gt;关于进程的创建，可以使用 &lt;code&gt;fork系统调用&lt;/code&gt; 创建一个新的进程。&lt;code&gt;fork&lt;/code&gt; 创建的新进程被称为&lt;strong&gt;子进程&lt;/strong&gt;，其内存内容与调用的进程完全相同，原进程被称为&lt;strong&gt;父进程&lt;/strong&gt;。在父进程和子进程中，fork都会返回一个值：在父进程中，fork返回子进程的PID（PID 可以理解为进程的编号，独一无二）；在子进程中，fork返回0。&lt;/p&gt;
&lt;p&gt;获取进程的 PID 可以通过 &lt;code&gt;getpid&lt;/code&gt;，直接返回当前进程的 PID。&lt;/p&gt;
&lt;p&gt;进程的退出则简单多了：&lt;code&gt;exit 系统调用&lt;/code&gt; 会使得调用它的进程退出。&lt;code&gt;exit&lt;/code&gt; 需要一个整数状态参数，通常0表示成功，1表示失败。&lt;/p&gt;
&lt;p&gt;还有一个操作，这对父进程适用：&lt;code&gt;wait 系统调用&lt;/code&gt; 返回当前进程的一个已退出的子进程的PID。如果调用者的子进程都没有退出，则等待这个子进程退出。如果调用者没有子进程，wait立即返回-1。&lt;/p&gt;
&lt;p&gt;以上便是目前需要了解的进程知识。进程需要学习的东西还有很多，后面继续讲。&lt;/p&gt;
&lt;h3&gt;管道&lt;/h3&gt;
&lt;p&gt;刚刚我们知道了进程是什么，而且还可以通过 fork 和 exit 来创建和退出进程。有意思的是进程不仅埋头苦干，还可以相互交流，他们之间交流的一种方式就是通过&lt;strong&gt;管道&lt;/strong&gt;。想象一下，小时候你可能对着一个水管说话，水管的另一头还有个小伙伴用耳朵听你说的内容。这就是管道，进程就是通过如此“原始”的方法沟通的。&lt;/p&gt;
&lt;p&gt;从上面的例子我们也可以看到，使用管道，必然有一个进程“说话”，另一个“倾听”，那我们该如何确定这个进程是在“说话”还是“倾听”呢？方法是&lt;strong&gt;文件描述符&lt;/strong&gt;。文件描述符相当于一个整数“门票”，它标记了对文件的操作的入口。比如说我们想往一个文件写入字符串，那么我们只需要向这个文件的写入文件描述符发送字符串；如果我们想读取一个文件，只需要从这个文件的读取文件描述符读取字符串。&lt;/p&gt;
&lt;p&gt;如果我们将管道看作文件，那么“说话”进程只需要向管道的写入文件描述符写入字符即可，“倾听”进程只需要向管道的读取文件描述符读取字符即可。（除了管道，UNIX 操作系统几乎把一切资源都视作文件，这是 UNIX 哲学的精髓）总的来说，使用管道，需要我们先创建管道的文件操作符，然后通过文件操作符来读取、写入信息。&lt;/p&gt;
&lt;p&gt;创建一个管道需要用到 &lt;code&gt;pipe 系统调用&lt;/code&gt;，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int p[2];
if (pipe(p) &amp;lt; 0) {
	printf(&quot;pipe() failed\n&quot;);
	exit(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在通过 &lt;code&gt;pipe&lt;/code&gt; 创建管道之前，我们定义了 &lt;code&gt;p[2]&lt;/code&gt;，这就是文件操作符。其中 &lt;code&gt;p[0]&lt;/code&gt; 是读取，&lt;code&gt;p[1]&lt;/code&gt; 是写入。&lt;code&gt;pipe&lt;/code&gt; 需要传递这样的整数数组作为参数，同时返回一个整数值，创建失败则返回负数。&lt;/p&gt;
&lt;p&gt;那么读取写入又是如何实现的呢？通过 &lt;code&gt;read&lt;/code&gt; 和 &lt;code&gt;write&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char buf = &apos;x&apos;; 

// 参数列表：文件描述符、传递内容的地址、传递内容的大小（字节）
write(p[1], &amp;amp;buf, 1);
read(p[0], &amp;amp;buf, 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解法&lt;/h3&gt;
&lt;p&gt;有了以上前提知识，我们就可以着手于 &lt;code&gt;pingpong&lt;/code&gt; 程序的编写了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;kernel/types.h&quot;
#include &quot;user/user.h&quot;

int main(void) {
    // 创建两个文件描述符、一个字节和一个pid变量
    int p2c[2], c2p[2];
    char buf = &apos;x&apos;;
    int pid;

    // 创建两个管道
    if(pipe(p2c) &amp;lt; 0 || pipe(c2p) &amp;lt; 0) {
        printf(&quot;pipe() failed\n&quot;);
        exit(1);
    }

    // 创建子进程
    pid = fork();
    if(pid &amp;lt; 0) {
        printf(&quot;fork() failed\n&quot;);
        exit(1);
    }

    // 通过pid判断当前进程是子进程还是父进程
    if(pid == 0) {
        // 子进程
        // 关闭不用的文件描述符
        close(p2c[1]);
        close(c2p[0]);

        // 读取父进程传来的字节
        if(read(p2c[0], &amp;amp;buf, sizeof(buf)) != 1) {
            printf(&quot;child: read failed\n&quot;);
            exit(1);
        }
        close(p2c[0]);
        printf(&quot;%d: received ping\n&quot;, getpid());

        // 写入字节
        if(write(c2p[1], &amp;amp;buf, sizeof(buf)) != sizeof(buf)) {
            printf(&quot;child: write failed\n&quot;);
            exit(1);
        }
        close(c2p[1]);
        exit(0);
    } else {
        // 父进程
        // 关闭不用的文件描述符
        close(p2c[0]);
        close(c2p[1]);

        // 写入字节
        if(write(p2c[1], &amp;amp;buf, sizeof(buf)) != sizeof(buf)) {
            printf(&quot;parent: write failed\n&quot;);
            exit(1);
        }
        close(p2c[1]);

        // 读取子进程传过来的字节
        if(read(c2p[0], &amp;amp;buf, sizeof(buf)) != 1) {
            printf(&quot;parent: read failed\n&quot;);
            exit(1);
        }
        close(c2p[0]);
        printf(&quot;%d: received pong\n&quot;, getpid());
        exit(0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>