<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Modo&apos;s Dreamland</title><description>No description</description><link>https://www.modo.org.cn/</link><language>zh_CN</language><item><title>Golang：依赖倒置不是循环依赖的万能解药</title><link>https://www.modo.org.cn/posts/2026-8-7/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/2026-8-7/</guid><pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这篇文章本来是两个月前的草稿。不过好像后来忘掉了，现在重新写了一遍&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;用 Typeless 辅助写的，受朋友推荐了下，还确实挺好用的，可惜还找不到足够好用的开源平替，以后写作都用这个了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;众所周知，Golang 项目的代码结构都比较趋向按模块划分，所以也会不可避免地遇到循环依赖的问题，当然，解决循环依赖的手段实际上有很多，其中一种被认为比较优雅的处理方式就是使用依赖倒置，也就是抽出来一个接口，以及很多文章都会推荐你这么去做&lt;/p&gt;
&lt;p&gt;但实际上，这种方式并不适用于所有场景，甚至在某些更偏向传统业务后端的项目里，大多数出现循环依赖的场景都不太适合用依赖倒置来解决。而循环依赖时到底什么场景应该用依赖倒置，什么场景不应该用呢&lt;/p&gt;
&lt;p&gt;简单来说，不同层级的模块间的循环依赖，可以按照依赖倒置来解决，而同层级模块之间的循环依赖，并不推荐靠依赖倒置来解决。什么是同层什么是不同层呢，诸如两个业务模块user和order，这就是同层级（至少通常来说是这样的），而一个业务模块user和一个基础设施模块比如消息队列，那就是不同层级（当然通常来说消息队列可能被划分为生产者与消费者两个模块，这里只是为了方便理解就归同一个模块了）。当然层级其实要结合实际业务需求来理解并划分，只不过通常在传统业务后端里默认都是这么划分，已经成常识了，但要是放到中间件或者一些轮子项目里可能就不太一样了。以下讨论所举的例子均基于传统web业务后端来理解&lt;/p&gt;
&lt;h3&gt;同层级模块产生循环依赖用依赖倒置会发生些什么&lt;/h3&gt;
&lt;p&gt;首先，需要提的一点是，通常来说，模块内部划分的 handler、service 和 repo 层之间，使用了依赖注入方法&lt;/p&gt;
&lt;p&gt;从代码层面来讲，比如，我们有一个模块 user 和一个模块 order，User 里面有一个方法 U 调用了 Order 模块，然后 Order 模块里有一个方法 O 调用了 User 模块，然后产生了循环依赖。那么这个时候，用依赖倒置是在 user 模块里面定义一个接口，然后 order 模块实现这个接口，然后，代码层面的循环依赖好像就被解决了。然而哪怕仅从代码层面讲，真的解决了吗？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// user 模块
package user

type Service struct {
	order *order.Service // 依赖 order 模块
}

func (s *Service) U() {
	s.order.O()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// order 模块
package order

type Service struct {
	user *user.Service // 依赖 user 模块
}

func (s *Service) O() {
	s.user.U()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个包互相 import，Go 直接编译不过：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import cycle not allowed
import &quot;example/user&quot;
    import &quot;example/order&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果改用依赖倒置，各自定义对方需要的能力抽象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// user 模块：定义 OrderAPI 接口
package user

type OrderAPI interface {
	O()
}

type Service struct {
	order OrderAPI // 只依赖抽象，不再依赖 order 包
}

func NewService(o OrderAPI) *Service {
	return &amp;amp;Service{order: o}
}

func (s *Service) U() {
	s.order.O()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// order 模块：定义 UserAPI 接口
package order

type UserAPI interface {
	U()
}

type Service struct {
	user UserAPI // 只依赖抽象，不再依赖 user 包
}

func NewService(u UserAPI) *Service {
	return &amp;amp;Service{user: u}
}

func (s *Service) O() {
	s.user.U()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通常来说，模块里面会采用依赖注入的方法，这也是比较普遍的。好，如果你真的是这么去做了，你就会遇到一个问题，在最上层依赖注入的时候，产生了死锁，然后就根本没法实现依赖倒置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 组合根 main.go：无论先创建哪个，另一个都还没就绪
func main() {
	userSvc := user.NewService(orderSvc)  // orderSvc 还没创建
	orderSvc := order.NewService(userSvc) // userSvc 还没创建
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候你可能想去解决这个问题，于是去翻文档或者问 AI，你可能会得到一些解决方法，其中一个叫懒加载，另一个叫属性注入。确实，这两个方法确实能解决这个问题，但是实际上只是在打补丁。而且我从个人观点出发，并不推荐在 Golang 的项目里面这么写，这其实是一种非常非常 Java 的写法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 属性注入：不通过构造函数注入，先把对象建出来再补上依赖
func main() {
	userSvc := &amp;amp;user.Service{}               // Order 依赖先空着
	orderSvc := &amp;amp;order.Service{User: userSvc}
	userSvc.Order = orderSvc                 // 事后补上，绕开注入死锁
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 懒加载：用到的时候才去获取依赖
package user

type Service struct {
	orderOnce sync.Once
	order     OrderAPI
}

func (s *Service) getOrder() OrderAPI {
	s.orderOnce.Do(func() {
		s.order = order.GetInstance() 
	})
	return s.order
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&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;blockquote&gt;
&lt;p&gt;在面向对象编程领域中，依赖反转原则（Dependency inversion principle, DIP）是指一种特定的解耦（传统的依赖关系建立在高层次上，而具体的策略设置则应用在低层次的模块上）形式，使得高层次的模块不依赖于低层次的模块的实现细节，依赖关系被颠倒（反转），从而使得低层次模块依赖于高层次模块的需求抽象。&lt;/p&gt;
&lt;p&gt;该原则规定：&lt;/p&gt;
&lt;p&gt;1.高层次的模块不应该依赖于低层次的模块，两者都应该依赖于抽象接口。
2.抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
该原则颠倒了一部分人对于面向对象设计的认识方式。如高层次和低层次对象都应该依赖于相同的抽象接口。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，依赖倒置本来就应该被用在不同层次的模块之间用来解耦，依赖倒置不应该被当成解决pkg之间的循环依赖的一种solution&lt;/p&gt;
&lt;p&gt;比如说我们有一个任务队列模块（这里为方便解释仍然当做一个模块）和业务里的task模块，Task 模块调用了任务队列模块的 Enqueue方法，任务作业模块调用了 Task 模块的更新任务状态方法，然后循环依赖了。那么，这个时候我们就应该用依赖倒置，因为 Task 模块本来不应该依赖于任务队列模块的具体实现。而且基础设施模块不会像那些业务模块一样采用几乎相同的依赖注入方式，所以不太会有前面的死锁问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// task 模块
package task

type TaskQueue interface {
	Enqueue(taskID string) error
}

type TaskService struct {
	queue TaskQueue
}

func NewTaskService(q TaskQueue) *TaskService {
	return &amp;amp;TaskService{queue: q}
}

func (s *TaskService) Submit(taskID string) error {
	return s.queue.Enqueue(taskID)
}

func (s *TaskService) UpdateStatus(taskID string, status string) error {
	// 更新任务状态
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// taskqueue 模块
package taskqueue

type TaskUpdater interface {
	UpdateStatus(taskID string, status string) error
}

type TaskQueue struct {
	updater TaskUpdater
}

func NewTaskQueue(u TaskUpdater) *TaskQueue {
	return &amp;amp;TaskQueue{updater: u}
}

func (q *TaskQueue) Enqueue(taskID string) error {
	// ...投递到消息队列
	return q.updater.UpdateStatus(taskID, &quot;running&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 组合根负责装配：双方的具体实现各自满足对方的抽象
func main() {
	taskSvc := task.NewTaskService(nil)      // 先创建，便于后续注入
	queue := taskqueue.NewTaskQueue(taskSvc) // task.TaskService 实现了 TaskUpdater
	taskSvc = task.NewTaskService(queue)     // taskqueue.TaskQueue 实现了 TaskQueue
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是我们就实现了高层次模块对低层次模块的解耦，只是在 Golang 的语法与代码里表现出来，循环依赖的错误被解决了&lt;/p&gt;
&lt;p&gt;所以循环依赖意味着什么呢？&lt;/p&gt;
&lt;h3&gt;循环依赖&lt;/h3&gt;
&lt;p&gt;产生循环依赖，意味着你的模块设计本来就有问题，只是对于不同层次的模块出现的设计问题，恰好可以用依赖倒置来解决，而对于同层次模块出现的模块设计问题，依赖倒置则无法较好地解决。&lt;/p&gt;
&lt;p&gt;Golang 的循环依赖错误应当被视为一种信号，而不是错误。只不过，它在 Golang 的语法里确实是错误的。&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;p&gt;依赖倒置确实是解决循环依赖的一种优秀手段，但它解决的前提是：两个模块之间存在明确的层级关系，高层模块不应该依赖低层模块的具体实现，而应该依赖抽象能力。在这种场景下，依赖倒置不仅解决了循环依赖，也让模块之间的边界更加清晰&lt;/p&gt;
&lt;p&gt;但是，如果两个模块本身处于同一业务层级，例如 &lt;code&gt;user&lt;/code&gt; 和 &lt;code&gt;order&lt;/code&gt; 这种业务模块之间产生循环调用，那么强行引入接口、懒加载或者属性注入，往往只是绕过了语言层面的限制，并没有真正解决模块设计上的问题，甚至可能让原本简单的业务关系变得更加复杂&lt;/p&gt;
&lt;p&gt;换句话说，循环依赖不是一个需要被消灭的语法错误，而是架构设计中的一个反馈信号&lt;/p&gt;
&lt;p&gt;好的设计不是找到一种技巧让循环依赖消失，而是在出现循环依赖时，重新审视模块之间的关系，让依赖方向符合业务本身的逻辑&lt;/p&gt;
</content:encoded></item><item><title>2026-7-30</title><link>https://www.modo.org.cn/posts/2026730/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/2026730/</guid><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;7月下旬回了杭州，然后在advx期间和几位好友面基了。说到这，advx给我们几个真心要做产品的倒是全给刷了呵，当然申得晚也确实是&lt;/p&gt;
&lt;p&gt;前天时候又从杭州回来，杭州夏天的天气实在是难受了。然后昨晚Veno说打Deadlock，然后发现拿来打游戏那台win本电源没带回来🥲，pd没撑过半小时然后歇菜了，好那估计暑假玩不了啥了，新买电源还不如来回一趟杭州划算，这一个月试试mac开vm推推旮旯得了&lt;/p&gt;
&lt;p&gt;八月，八月接下来猜是会很忙，好像都堆到这个点上了&lt;/p&gt;
&lt;p&gt;然后本来想计划九月初去上海今年的Kubecon China玩玩见见世面，然后偶然一看学校安排，时间刚好又和短学期的答辩重合了呵呵，好那希望到时候我能battle的过吧&lt;/p&gt;
</content:encoded></item><item><title>2026-7-23 以后，以及以后</title><link>https://www.modo.org.cn/posts/2026723/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/2026723/</guid><pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;目前的想法。 嗯，在旭哥这至少会待上比较长的一段时间，至少如果是去小厂做非核心业务的产出是肯定不如我在这边写后端学到的多，毕竟也是正儿八经的商业项目，而且对我来说很好的一点是这边后端业务也不是传统的那种枯燥的web后端业务，而是会偏infra一些的计算平台，场景也非常切合用go后端的场景。然后也都是认识的人，也有工资拿，除此以外也在平常交流里学到了很多非常重要的东西，诸如旭哥之前强调的工程师一定要在干中学，这半年来算是深有体会，我想我现在上来学任何工程领域的新东西都不会上来就去看任何课程的，哪怕课程确实很优质，比如最早碰云原生这块的时候看过一点阿里云的云原生公开课来着，但是能指望就靠这个学明白云原生学明白kubernetes吗，我想从运维上得真在集群上玩过k8s，从开发上得真在云环境里接触服务发现和治理之类的问题，再比如DDD的设计思想，如果是去知乎上找篇精美的万字长文阅读，读完之后大概率还是p都不会。总之在这边接触了真实的业务、真实的生产场景前后，很多对于后端、对于技术的认识跟之前比都是非常非常不同的，我想对我来说是一个非常难得的机遇吧，以及包括为什么要参与开源，因为确实能接触到真实的生产环境里的很多的框架级问题，尤其对于学生来说，总之真实的业务场景是非常能锻炼人的，之前想过觉得按那套所谓的后端范式去写web项目其实是没什么成长的，单体web也好，微服务也好，如果仅仅是作为个人项目去写是没有多少收获得，更多还是为了包装简历，而从提升实际的水平来考虑其实应当去多造轮子，因为轮子对个人来说是有真实的业务场景在的。写同样一个后端，假装自己有十万流量和真的有十万流量的场景还是有很大不同的&lt;/p&gt;
&lt;p&gt;计划会是可能在明年三四月去找互联网的后端实习，嗯其实最好是能做云相关的业务。但也只是计划，我无法预料未来的我会如何思考，以及未来会发生什么不一样的事，几个月前Veno老师还打算去实习来着，结果转念一想就去尝试做创业产品了。这段时间来，校内校外有幸见识到了很多不同的人，旭哥也是从高薪工作里跳出来创业的，也有见到从大厂出来做自媒体的，诸如此类的，所以，自己也有了很多不一样的思考吧，记得很早很早以前和某位朋友闲聊说之后就是做项目、然后实习然后投大厂，然后朋友说这样生活也太无聊了吧，唉现在想来确实不无道理，很多时候我不免怀疑这仍是一种学生思维在影响自己，总想按照一种既有的路线来达成某种特定结果，那么一是既有路径是可复制的，必然会挤满了人与竞争，二是这条固定路线带来的生活，未必是自己所喜欢的。想到旭哥之前所说的，既然他之前工作的时候时薪有一千（笑），那么对他来说为什么不每天工作两小时，🤗好吧当然对我来说我很显然肯定没有这个能力，也没有在大厂工作过，没什么资格来评价这个事，只是想说见识到更多人之后，对我来说在某些生活态度上，有了更多思考吧&lt;/p&gt;
</content:encoded></item><item><title>2026-7-9</title><link>https://www.modo.org.cn/posts/202679/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/202679/</guid><pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;台风，然后这不明所以的短学期线下终于停课&lt;/p&gt;
&lt;p&gt;于是赶紧买了票，在高铁回金华&lt;/p&gt;
&lt;p&gt;没在杭州感受台风过，以往在金华台风和雷阵雨差不大多吧。但是看情况待在学校这寝室给我一种越来越不安全的感觉，很怀疑宿舍这老门老窗的抗风能力，离开前把桌上都放安全了，不是没有吹凌乱的可能。带着四台机子全打包回家了，剩下比较值得担忧的就是显示器了&lt;/p&gt;
&lt;p&gt;这几天尝试了把台openwrt拿来作k3s的代理节点，然后得折腾很多偏门的脏活，各种本来成套的工具方案，诸如kube-prometheus-stack，到openwrt上受限会有各种奇怪的问题，得为openwrt单独搭一套。嗯 感觉对Kubernetes这一套还是得多玩，感觉未知得还有很多，越接触，就越来越多&lt;/p&gt;
&lt;p&gt;此外最近算是彻底想明白了自己这一年以来一直电子ed的原因。实则并非是电子ed，而详细地说是“比较难以沉浸在gaming中”，而往常玩得多的又是需要沉浸点的世界或者剧情一类。这是什么造成得呢，答曰： 集体住宿。过去一年一直怪怪得有种感觉，然后啥都看不进去了，gal也推不动了，番也有时候看不进去，其他的游戏也玩不进去，放在中学Outer Wilds这种游戏我想我不得连着玩通宵到通关。然后做过很多尝试，以为是中学时习惯大屏幕的沉浸感，遂调了座椅，换了个2k的大显示屏，结果于事无补。后来终于想明白：在这么一个挤满5人的集体空间里，我在尝试沉浸于任何剧情的时候总会有一种背后有人在盯着我或者走来走去的感觉，&lt;s&gt;哦或者害怕fps哥大叫给我吓一跳&lt;/s&gt;，总之这种感觉对于我个人来说非常得不自在，我很忌讳在我鉴赏任何剧情时周围有任何人。这也很好解释了为什么半夜时候室友都睡觉了，看番推gal都会更有兴致。可能更多是个人因素，但和我有同样感觉的遇到恐怕不止一个。嗯不想进一步对这个集体空间的某些事情作出某些锐评，只能说尽早远离这种的集体住宿对于改善身心来说是大有好处的&lt;/p&gt;
</content:encoded></item><item><title>2026-7-4</title><link>https://www.modo.org.cn/posts/202674/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/202674/</guid><pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;学校期末周结束，七月开始&lt;/p&gt;
&lt;p&gt;接下来会很丰富了吧，很多事情要做，paramer这边的工作，然后Veno有个新的agent产品要做了，所以可能借机要去一趟adventurex，虽然到现在那边还没审批我们几个人的申请&lt;/p&gt;
&lt;p&gt;再还有最近从mias那边了解到的做的很好的dubbogo的社区，以及围绕着的一批做开源的高校学生以及圈子，多少是有点相见恨晚的感觉。记得前几个月那会儿，尝试去参与一些开源，看杭电助手Bird前辈的&lt;a href=&quot;https://blog.aflybird.cn/&quot;&gt;博客&lt;/a&gt;，然后看他们那批人那时候一步步的开源经历，然后就发现到我这就，开源之夏今年半死不活得，然后感兴趣的云原生方向的很多项目从前几年devops大建设时期到现在已经不怎么活跃了吧，感觉很多比较贡献者友好的还是国外的开源社区偏多。然后后来翻了翻看dubbogo的commit log意外看到两个杭助的老学长24年的时候也有在里面贡献过，很巧的事，神奇的感觉&lt;/p&gt;
&lt;p&gt;然后继续捣鼓捣鼓homelab和写玩具项目。又回到了充满干劲的状态&lt;/p&gt;
&lt;p&gt;想想最近半年来真算得上是飞速成长吧，有种看几个月前自己还是小透明的感觉哈哈，不过那会儿大家都在安慰我才大一 不要急，想想，马上 就快大二了啊。那时写的文章现在的我看起来多少有点不堪入目了，留着当黑历史吧）&lt;/p&gt;
</content:encoded></item><item><title>2026-6-27</title><link>https://www.modo.org.cn/posts/2026627/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/2026627/</guid><description>梅雨，会是倒霉的雨吗</description><pubDate>Sat, 27 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;梅雨，会是倒霉的雨吗&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最近还在期末周，没怎么写过了。这段时间杭州每天天气都很糟糕&lt;/p&gt;
&lt;p&gt;傍晚的时候比较意外高中班主任来这边，然后就几个人小聚了一下。然后感觉呃，包括高中同学吧，从接触了很多东西开始，说实话更多时候没有了什么共同话题。对我来说好像，已经很久很久都没有想起过，那些模糊的中学时期什么事了。带着人来咨询一下这边，可能为了填志愿吧 问了 嗯各自的情况，吃了顿饭，逛了逛然后差不多我也就是半沉默的样子，也找不出什么话题，就边应和下其他人的说法，谈论些什么 问了些什么然后好像就这样吧。也不想说一些啥了吧，讲清楚恐怕也容易得罪人，我想有些隔阂总是会必然的，就像之前爸妈理解不了我跟他们说得现在大学到底是个什么状况一样，曾经的班主任包括我想也理解不了很多事情吧，说出来恐怕也会被当异类看，虽然过去在我高中班主任眼里，可能本来我也就算足够异类的那一批人了&lt;/p&gt;
&lt;p&gt;想想高中，高中生活呵呵也不算友好吧，记得那会儿入学开始就逐渐能看到知乎上越来越多开始喷学校每况愈下的管理，那会算是转折点了吧。浙江小地方，毕竟不是在杭州或者宁波这种地方上中学，并没有那种丰富的高中生活，讲起来有点意思的倒不如是高三经常逃晚自习摸去丽泽湖旁边的演奏厅，黑灯瞎火地弹琴，以及和朋友去空教室偷偷看当时我推第二季和亚托莉的漫改。其实也就是一所非常典型的高中吧，也没太多好回忆的，我恐怕也是一种没有”母校“概念的人，总之更多对我来说学校就是学校、人就是人吧&lt;/p&gt;
</content:encoded></item><item><title>2026-6-7 坑与坑</title><link>https://www.modo.org.cn/posts/202667/202667/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/202667/202667/</guid><description>踩过的坑与好像踩过的坑</description><pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;挂了名在服务外包上，自己倒是没怎么参与，毕竟是水赛，然后顺便就找了比赛答辩的机会去南京玩几天。此时坐在摇晃的大巴上刚被晃醒，无聊途中就想想先从硬核的角度总结下这一年来技术学习的经历和弯路，以及产生的一些极其重要的教训，或者说此时的我看待早期正在按照&lt;a href=&quot;https://csdiy.wiki/&quot;&gt;csdiy.wiki&lt;/a&gt;老老实实去学CS61A和CS61B的自己，会提出怎么样的异议&lt;/p&gt;
&lt;p&gt;那我想就从一年前说起，那时的我正在看Crash Course Computer Science这门更偏向是科普的课，里面并没有讲任何过于硬核的知识&lt;/p&gt;
&lt;h3&gt;2025.7 &quot;Computers are not magic!&quot;&lt;/h3&gt;
&lt;p&gt;今天的我看来，最初的这门课带给我的启蒙是很大的，说的不仅仅是从知识层面的启蒙，直到今天我仍然高度肯定当时学这个的选择。至于为什么，首先它讲解了从最初的打孔纸带到互联网的整个计算机科学的发展，而且从中非常形象地讲述了最最最重要的一个概念“抽象”是怎么让计算机飞速发展起来的，”抽象“这个概念对于在计算机领域各个方向以后不管是学什么课，做什么项目都是一定是会频繁用到的，这是构建整个计算机世界的基石 &lt;em&gt;&lt;strong&gt;A new level of abstraction&lt;/strong&gt;&lt;/em&gt;。还有一个点是这门课涵盖了计算机的各个方面，操作系统、计组、计网、软件工程等各个方面，这让我能意识到我所做所学的内容究竟是什么方向的，各个方向上所做所学的内容又应该是什么样子的。哈其实现在有时候需要补点计组的基础知识我也会回过去看看&lt;/p&gt;
&lt;h3&gt;2025.8 关于MIT Missing Semester&lt;/h3&gt;
&lt;p&gt;当时看&lt;a href=&quot;https://csdiy.wiki/&quot;&gt;csdiy.wiki&lt;/a&gt;接下来推荐学MIT Missing Semester，现在看来当时很明智的选择是就草草学了shell和git两节课没花精力看其他，missing semester的大多数内容只是作为补充信息差就够了，并不真正值得去投入地学，快速过一遍概念，大概知道是干嘛的就够了，然后等真正在生产中用到的时候自然就会掌握，就像没人会专门去学linux怎么用一样&lt;/p&gt;
&lt;h3&gt;一段重要的插曲：CS与SE&lt;/h3&gt;
&lt;p&gt;在接下来的一段经历之前，需要说明的是Modo那个时候已经决定了做后端开发的方向。除此以外，还需要阐述一下一个对于新人来说非常需要知道的概念差异：CS与SE，也就是Computer Science与Software Engineering，也有认识偏CS方向的，之前其实一直暗暗能感觉到很多方面策略的不同，后来看到Mias老师的文章&lt;a href=&quot;https://www.mias.moe/posts/path-for-se-learning&quot;&gt;一条 SE 自学之路&lt;/a&gt;我觉得就非常清楚了，对这两个概念讲得非常透彻，总之一个是研究积木怎么造，一个是积木怎么搭&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在现代社会里，包括曾经的小萌新时期的我在内，很多人会混淆计算机的各个方向，对这个巨大的世界认知往往模糊不清，认为一个人能完成整套业务需求、做web的一定会写数学算法、做理论的一定能写好项目代码等等&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所有学计算机的学生大概都会分化成这两个方向，最终从事的工作基本也是。而这两个方向的学习内容和学习方法论是很不同的，这点会在接下来这段对于CS61A与CS61B的反思中体现。而我所走的后端开发方向很明显就是SE方向&lt;/p&gt;
&lt;h3&gt;2025.9 反思所谓的神课CS61A与CS61B&lt;/h3&gt;
&lt;p&gt;那时我连着学了所谓的计算机入门“神课”CS61A与CS61B，不过61B一半就stop了后面就开始写后端去了。那时我还没有对CS与SE两个方向的学习方式差异有清晰的认识，于是就这么学下去了。当时是和另一个做底层方向的朋友一起学和讨论的，现在来看很明显他是CS方向，很多学习方式会对比SE方向，需要学院派一些。然后接下来我想谈的是CS方向与SE方向的差异在61A与61B两门课的学习上体现出来的侧重点的区别。CS61B的内容是数据结构与算法，对于CS方向，侧重点应该在各个具体数据结构的学习；而对于SE方向，侧重点应该在关注数据结构是如何被包装在实际项目里进行运用的。所以从我的角度出发，61B的后半部分讲得是具体数据结构的各种细节的部分其实并不需要细学。同样的对于CS61A，目前看来帮助比较大的是前三分之二，而后三分之一虽然当时学的时候觉得lisp语言体现出来的程序即数据的概念确实很有意思，以及元编程、尾递归优化啥的，当时花了不少时间去研究，但对于我现在的方向来说这些并不重要，或者说学它的优先级不太高。当然我也不否认这两门课对于当时刚入门的我来说确实还是有很多部分挺有用的，以及课程和练习也是非常有意思的。同时，再从SE的方向出发，或许当初其实根本就不需要学这两门课？，而是上来就直接干实际项目，不会就去查文档、问ai，在生产环境里学。所以实际上现在看来主要还是在这两门入门课上花的精力有点太多了。&lt;/p&gt;
&lt;h3&gt;2025.12 SE-工程师永远是在干中学&lt;/h3&gt;
&lt;p&gt;那时候过完了61A 61B，然而发现一个严重的问题，大概两个月前，我说我要走后端方向，所以就拷问自己：你学了这么久会后端吗。发现自己好像除了curd好像啥都不会，也没有写过一点后端项目。然后那个时候我做了一件如今看来十分学生思维的一件事，就是开始问Veno和smallfang后端该怎么入门、怎么学web后端，当时感觉是被后端这一整个大领域唬住了，甚至开始焦虑这个问题，那时好像有几天持续处于这种莫名其妙的迷茫感中。但是到真正上手去写项目时，这些问题其实根本就不存在了，先前的这种所谓的迷茫是没有意义的。现在看来，哪怕是0基础，直接上手硬干就是了，所谓的后端的学习就应该是在实际项目中解决问题，在这个过程中不断查文档，尤其现在更方便可以问ai，然后就会写后端了，后来接触了旭哥这边项目开发也是一直听到强调“工程师永远是在干中学”的观念，想来这算是非常重要的lesson，虽然说很早之前就在说任务驱动，但是没经历过教训还是不够深刻吧，而SE的学习过程就应该是这样任务驱动的&lt;/p&gt;
&lt;h3&gt;2026.2~ 对浮于表面的技术栈袪魅&lt;/h3&gt;
&lt;p&gt;从这段时间开始，Modo在后端开发上算是进入状态了，也是感觉自己在技术上飞速进步的一段时间，中间其实做了很多事吧，不管是最早写着玩的一些玩具后端，再到跟着一些go后端项目敲和之前写的比较难评的开源实习，不过中间很多未必能量化，就一笔带过了，然后直到现在就是写Paramer的后端。这中间没算踩到什么坑，主要幸运在一直都有前人愿意指导吧。中间很有感触的一点是对所谓的“技术栈”袪魅，什么意思呢，比如对于后端开发来说，不管是拿Java写还是Go还是Rust写，写后端很多的核心是不会变的，从业务角度上，非常核心的能力是业务开发里对需求的理解、怎么拆分、根据需求进行技术选型以及和产品经理的角色的沟通等等。技术角度上， 比如不管哪个语言写的后端总会遇到双写一致性以及高并发之类的技术问题，架构上的问题也是一样，而所谓的语言只是工具。其他的很多技术栈、各种名词也是一样的，比如所谓“会Rabbitmq”，这仅仅只是会一门工具，而其中的核心应该是能够解决消息队列在应用时会遇到的各种常见的技术问题。掌握了这些核心的东西之后，应用各种技术栈上去其实是很快的事&lt;/p&gt;
&lt;h3&gt;Now&lt;/h3&gt;
&lt;p&gt;总之要是回到一年前那时候，我可能会快速把那几门入门公开课花一个月时间快速过一遍就好，当初看到的csdiywiki的很多东西也是未必跟得上时代了，也没有对个人情况和方向有特化，嗯然后就直接上手干实际项目了吧，那样或许会更好吧，但或许又是因为走过这些弯路吧，所以现在能清晰认识到总结出来的这些教训呢，过去的时间无法重来，我想我也无从得知了&lt;/p&gt;
</content:encoded></item><item><title>2026-5-20</title><link>https://www.modo.org.cn/posts/2026520/2026520/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/2026520/2026520/</guid><pubDate>Wed, 20 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这次把giscus评论系统接入fuwari了，看到最简单应该是这篇了&lt;a href=&quot;https://jk.sb/posts/giscus/&quot;&gt;https://jk.sb/posts/giscus/&lt;/a&gt;，按这篇文章的配置自动会适配主题颜色和深色模式&lt;/p&gt;
&lt;p&gt;然后是一回过头来发现好像学校里很多乱七八糟的事水课啥的又到ddl了。我想我目前要做想做的任何事应该都与学校完全脱钩了，但不得不说这种时常来一下的干扰仍然是挺影响节奏的。想起来最舒服的还是去年九月十月，那会儿是没有任何奇奇怪怪的事情，还是怀念那个时候的效率
&lt;img src=&quot;assets/IMG_20260520_105448.png&quot; alt=&quot;IMG_20260520_105448&quot; /&gt;&lt;/p&gt;
&lt;p&gt;另外原本以为开源之夏一直到五月份没消息今年可能不办了，前几天看到公众号发文说今年还是会有的，到时候再看看有没有感兴趣的吧，感觉不出意外今年应该会出很多agent相关的吧，经历了上次那件糟糕的开源经历后不得不说找到一个符合技术栈、方向感兴趣、社区培养好、导师人好的项目然后再成功申请上还是没有那么容易的&lt;/p&gt;
</content:encoded></item><item><title>2026-5-11</title><link>https://www.modo.org.cn/posts/2026511/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/2026511/</guid><pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;给博客翻新了一下，之前用的Hexo的vivia主题有不少小问题，于是选择原Vivia迁移后的Astro框架的Fuwari，就顺便把博客从Hexo转到Astro了。不过坏处是Fuwari没有官方集成过评论系统，之后可能还得折腾一下，能看到有的fuwari博客已经用上twikoo或者Giscus，所以问题应该不大&lt;/p&gt;
&lt;p&gt;五一过后最近总算是平淡了点，四月的后半在我印象里是挺杂乱的，连续去了好多地方，中途顺便还应付掉了没成功请掉的期中考，好像很忙但又不知道在忙些啥，就想到昨天理了理好久没理的头发，由于长了以后硬发质摸起来实在明显，理发师直接推荐我以后找个机会去烫个黑人烫，当然她说别找她烫就行~ 。最近能抽出些时间干点之前一直计划的事比如装修博客之类的，以及此前推到一半pending的牧羊人。。不过这种推废萌作的节奏倒是让我感觉挺舒服的，这种情况下半夜推gal的感受就像是给自己的生活不时注入点兴奋剂，总之还是挺享受的~ 另外接下来就应该是要正式开始ParamerV3的工作了。&lt;/p&gt;
&lt;p&gt;想了下往后的博客我想我会更多写经历和思考，当下完全纯粹的知识型技术博客面对llm与agent这种获取技术知识的新方式来说价值是在缩小的，技术博客的知识本身也是可替代性强的内容，并非依赖某个特定网站或者平台不可。而且想到那些之前让我受益匪浅的博客内容，通常来说都是作者的某些生涯经历或者观点，而不是某个非常具体的技术知识&lt;/p&gt;
</content:encoded></item><item><title>2026-4-16</title><link>https://www.modo.org.cn/posts/2026-4-16/2026-4-16/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/2026-4-16/2026-4-16/</guid><pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Just a Special Day&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;开香槟&lt;/p&gt;
&lt;p&gt;熟悉了一下Paramer项目的一步步演变&lt;/p&gt;
&lt;p&gt;更多内容请移步 &lt;a href=&quot;https://v3n0.top/post/paramer/0-preface/&quot;&gt;Veno的Paramer开发记录博客&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;香槟&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./xb.png&quot; alt=&quot;开香槟！&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;听紫老师讲Paramer开发血泪史~&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./tr2.jpg&quot; alt=&quot;听紫喵讲血泪史~&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;血泪史~&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./tr1.jpg&quot; alt=&quot;血泪史版书~&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;紫老师的不可能三角理论&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./tr.png&quot; alt=&quot;紫老师独创不可能三角理论&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>2026-4-2</title><link>https://www.modo.org.cn/posts/date-2026-4-2/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/date-2026-4-2/</guid><pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;很忙碌的一段最近时光，一边先是写项目的agent监控日志模块，中间不知不觉就用到了OpenTelemetry，发现是在CNCF下的一个孵化项目，顺便看了看OpenTelemetry的社区，感觉是非常有活力的一帮人，他们的社区讨论会议是完全公开的，好奇还试了下进去。不过另外看到比较可惜的是OpenTelemetry社区的LFX项目在刚好不久之前结束申请😳&lt;/p&gt;
&lt;p&gt;说到OpenTelemetry，虽然感觉还没有知名到很大众化的地步，但是现在越来越多的企业和项目已经开始用它了，比如说CherryStudion内部trace追踪采用的就是OpenTelemetry来实现。agent、分布式架构工具和可观测后端都在积极对接生态，我想OpenTelemetry想要实现的可观测标准在现在agent时代是肯定喝到汤了，可以想象这个项目应该是很有未来的。OpenTelemetry整个项目的理念是极好的，供应商中立，还可以选择用zero-code的方式，几乎是无痛接入otel collector，引入之后也不影响可观测数据走其他标准，对可观测后端兼容性也是非常好的，现在看来OpenTelemetry这一块应该是将要成为CNCF的又一个k8s级别的项目&lt;/p&gt;
</content:encoded></item><item><title>OpenTelemetry：可观测性需求的一步步演变</title><link>https://www.modo.org.cn/posts/opentelemetry%E4%BD%BF%E7%94%A8%E7%AC%94%E8%AE%B0/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/opentelemetry%E4%BD%BF%E7%94%A8%E7%AC%94%E8%AE%B0/</guid><pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Logs&lt;/h2&gt;
&lt;p&gt;在传统的单体应用中，通常通过传统的“打日志”的方式去实现最初级的可观测性需求，我们通常只是在代码里提供一些日志输出，比如 &lt;code&gt;log.Printf()&lt;/code&gt;一下，然后就能在应用的控制台看见日志输出。再高级点，可能就是分为&lt;code&gt;[Warn]&lt;/code&gt; &lt;code&gt;[Info]&lt;/code&gt; &lt;code&gt;[Error]&lt;/code&gt; &lt;code&gt;[Fatal]&lt;/code&gt;之类的日志输出，这样的简单监控对于单体应用排查问题基本也是够用的。&lt;/p&gt;
&lt;h2&gt;Traces, metrics and logs&lt;/h2&gt;
&lt;p&gt;随着分布式、微服务复杂架构以及复杂业务逻辑的兴起，简单的“打日志”方式在已经不能满足对整个应用的可观测性需求，一个请求在微服务中可能会跨多个服务来处理，每个服务如果都是采用简单的日志输出打印到控制台上，这样的遥测数据很明显是非常杂乱低效的。于是在新时代下诞生了一套新的遥测方法，其中核心是链路追踪，一般称为trace，简单来说比如在微服务架构里追踪一个请求，这个请求每经过一个服务就产生一个span，每个span下还可以根据当前服务内的业务逻辑再带一串subspan，一串subspan组成了代表这个服务的span，而这一串span则组成了这个请求经过各个服务的最终整个trace数据。除了trace以外，这套新遥测数据还包括metric和log，metric是什么呢，就是直接的对应用内要观测的数值输出，比如网络延迟、运行时间等等，至于log就是之前讲的传统的事件日志输出。&lt;/p&gt;
&lt;h2&gt;OpenTelemetry&lt;/h2&gt;
&lt;p&gt;这套trace、metric和log的云原生时代下的新的可观测方式固然是非常有效的，但是新问题又来了。
对于trace、metric和log这套遥测数据的接收，通常是会采用专门的可观测性后端来接收（什么是可观测性后端呢，就是像 Jaeger、Prometheus这样的接受这套遥测数据进行处理并最终以仪表化的方式呈现出来的应用）。而问题在于不同的可观测后端有各自对遥测数据的标准，有各自的SDK，有的可观测性后端是以trace为核心设计的，有的又是以观测metric为主，最终导致应用侧需要为不同可观测性后端分别接入不同的 SDK 或 exporter，导致会被供应商锁定，以及重复埋点，迁移的成本高，此外还有就是trace、metric、和log根据不同供应商设计，导致之间关联语义不统一。&lt;/p&gt;
&lt;p&gt;而OpenTelemetry就是为了“统一”这套标准而出现，实现了供应商无关。注意此处并不意味着强制所有可观测性后端都必须采用 OpenTelemetry 自己的一套存储模型或内部实现，OpenTelemetry 更像是一层位于应用与可观测性后端之间的标准化抽象层。实际上，OpenTelemetry是通过一套中间层colletor在应用和可观测性后端之间实现遥测数据解耦，并提供了统一的SDK和遥测数据协议OTLP，开发者通过SDK开发应用的遥测数据输出，这套数据经过collector后就能实现对接各个可观测性后端，也就是供应商无关。&lt;/p&gt;
&lt;p&gt;而OpenTelemetry Collector 作为中间层负责：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收遥测数据&lt;/li&gt;
&lt;li&gt;数据处理与转换&lt;/li&gt;
&lt;li&gt;batch、filter类似的 pipeline 操作&lt;/li&gt;
&lt;li&gt;将数据导出到不同的可观测性后端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里不对Collector展开，总体内部是有一套比较复杂的处理方式，同时Collector也是OpenTelemetry最核心的部分。&lt;/p&gt;
&lt;h2&gt;Agent&lt;/h2&gt;
&lt;p&gt;另外随着ai agent的兴起，为了实现对agent的可观测性，OpenTelemetry的可观测性愿景被发现在agent领域也是非常合适，trace对于agent的行为链路追踪是非常合适的，metric可以用做显示对应的token消耗，能看到有非常多agent相关的项目正在集成OpenTelemetry。&lt;/p&gt;
</content:encoded></item><item><title>业务小模块日记【2】</title><link>https://www.modo.org.cn/posts/%E4%B8%9A%E5%8A%A1%E5%B0%8F%E6%A8%A1%E5%9D%97%E6%97%A5%E8%AE%B02/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/%E4%B8%9A%E5%8A%A1%E5%B0%8F%E6%A8%A1%E5%9D%97%E6%97%A5%E8%AE%B02/</guid><pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Ouath2.0的工作流程&lt;/h3&gt;
&lt;h4&gt;Ouath2.0&lt;/h4&gt;
&lt;p&gt;ouath2.0是一种授权协议
它的存在是为了“一个应用如何在不拿到用户密码的情况下，访问用户在另一个系统的数据？”，平常网站里跳出来的第三方登录诸如使用google账号登录、微信登录实际上用的都是这一套协议，从而实现了比如你能够用google账号授权登录一个网站，但是网站拿不到你的google账号密码&lt;/p&gt;
&lt;h4&gt;Ouath2.0中大概可以分为这几个交互主体：&lt;/h4&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;li&gt;资源服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Ouath2.0流程：&lt;/h4&gt;
&lt;p&gt;以下是一种安全的通常实现&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用户&lt;/strong&gt;打开网站 example.com&lt;/li&gt;
&lt;li&gt;网站发现&lt;strong&gt;用户&lt;/strong&gt;尚未登录 ——&amp;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;li&gt;授权成功之后&lt;strong&gt;授权服务器&lt;/strong&gt;返回临时&lt;em&gt;code&lt;/em&gt;, 用户浏览器带上&lt;em&gt;code&lt;/em&gt;重定向到之前给的重定向地址app.com/callback?code=123&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网站后端&lt;/strong&gt;收到&lt;strong&gt;网站前端&lt;/strong&gt;带来的临时&lt;em&gt;code&lt;/em&gt;，并拿着该&lt;em&gt;code&lt;/em&gt;去找授权服务器换&lt;em&gt;Accesstoken&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权服务器&lt;/strong&gt;确认并发送&lt;em&gt;Accesstoken&lt;/em&gt;给&lt;strong&gt;网站后端&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网站后端&lt;/strong&gt;使用&lt;em&gt;access_token&lt;/em&gt;调用&lt;strong&gt;资源服务器&lt;/strong&gt;获取相应资源
//后两条是流程之后比较通用的做法&lt;/li&gt;
&lt;li&gt;后端根据用户信息建立自己系统的登录态（session / JWT）&lt;/li&gt;
&lt;li&gt;后续请求只使用自己系统的登录态&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>业务小模块日记【1】</title><link>https://www.modo.org.cn/posts/%E4%B8%9A%E5%8A%A1%E5%B0%8F%E6%A8%A1%E5%9D%97%E6%97%A5%E8%AE%B0-1/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/%E4%B8%9A%E5%8A%A1%E5%B0%8F%E6%A8%A1%E5%9D%97%E6%97%A5%E8%AE%B0-1/</guid><pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;业务逻辑-cdn持久化存储缓存以及更新cdn缓存&lt;/h1&gt;
&lt;h3&gt;这里以通过微信登录时缓存微信头像图片为例&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;cdn缓存是什么：&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cdn是网络上提供缓存服务的节点，用于减轻访问压力，并降低对第三方的依赖
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如用户在通过微信登录的时候会提供自己头像的url， 当我们不使用cdn缓存的时候，由于微信头像的url是不稳定的，当你的微信头像更换之后，原来的url会失效，最终导致网页根据该url解析的头像图片失效&lt;/p&gt;
&lt;p&gt;而如果采用cdn缓存，会将当前url的图片缓存到cdn节点，之后网页头像图片的获取就从cdn来，哪怕原头像图片对应的微信url失效，网页头像依旧正常&lt;/p&gt;
&lt;p&gt;接下来是更新cdn缓存和网页头像的一套业务逻辑模块，头像url分为以下三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;userInfo.AvatarUrl为新头像：当前这次登录时获取到最新的微信头像&lt;/li&gt;
&lt;li&gt;user.Avatar为网页头像：网页显示的头像&lt;/li&gt;
&lt;li&gt;user.PermanentAvatar为获取cdn缓存的url&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;···
······
//在覆盖之前捕获之前存储的旧头像
oldAvatarUrl := getUserProperty(user, wechat_avater_url)
setUserProperty(user, wechat_avater_url, userInfo.AvatarUrl)
//对网页头像操作：如果网页头像为空/默认/旧,则设置网页头像为新头像
if user.Avatar == &quot;&quot; || user.Avatar == organization.DefaultAvatar || user.Avatar == oldAvatarUrl {
			user.Avatar = userInfo.AvatarUrl
		}
//对cdn缓存头像图片操作：如果缓存不存在/旧头像与新头像不一样，则更新cdn缓存头像
if oldAvatarUrl != userInfo.AvatarUrl || user.PermanentAvatar == &quot;&quot; {
        //上传新头像到cdn缓存并返回新cdn缓存的url
			permanentAvatarUrl := getPermanentAvatarUrl(user.Owner, user.Name, userInfo.AvatarUrl, true)
			if err != nil {
				log.Printf(&quot;Failed to upload OAuth avatar for user %s: %v&quot;, user.Name, err)
			} else if permanentAvatarUrl != &quot;&quot; {
				user.PermanentAvatar = permanentAvatarUrl
				if user.Avatar == userInfo.AvatarUrl {
				//将网页头像从新头像url(第三方不稳定url)改为获取cdn缓存的稳定url
					user.Avatar = permanentAvatarUrl
				}
			}
		}
······
···
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Swift6之后更现代的REST API方法</title><link>https://www.modo.org.cn/posts/swift6%E4%B9%8B%E5%90%8E%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84rest-api%E6%96%B9%E6%B3%95/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/swift6%E4%B9%8B%E5%90%8E%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84rest-api%E6%96%B9%E6%B3%95/</guid><pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;目前网上检索到的关于Swift的REST API Call实现很多已经在swift6之后算是过时了的，Swift6强化了并发安全，以及URLsession API也相应增加了async版本（旧版本使用callback），更modern的方法是使用async/await以及Task来构建REST API Call&lt;/p&gt;
&lt;p&gt;以下均使用URLsession进行REST API Call：&lt;/p&gt;
&lt;p&gt;在旧版Swift中, Get接口是这么call的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        func fetchPage(completion: @escaping (Result&amp;lt;PageData, Error&amp;gt;) -&amp;gt; Void) {

        let url = URL(string: &quot;http://127.0.0.1:8080/community/page/get&quot;)!

        URLSession.shared.dataTask(with: url) { data, response, error in

            if let error = error {
                completion(.failure(error))
                return
            }

            guard let data = data else {
                completion(.failure(URLError(.badServerResponse)))
                return
            }

            do {
                let result = try JSONDecoder().decode(PageData.self, from: data)
                completion(.success(result))
            } catch {
                completion(.failure(error))
            }

        }.resume()
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大量使用了回调函数。
另外值得说的是在Swift6中xcode默认会给没有显式隔离的代码视作@MainActor,也就是默认跑主线程，所以在老式写法里对于&lt;em&gt;let result = try JSONDecoder().decode(PageData.self, from: data)&lt;em&gt;这里要在&lt;/em&gt;PageData&lt;/em&gt;的结构体上标注一个&lt;em&gt;nonisolated&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Swift6以后，新式Get Call 的async/await写法是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    func fetchPage() async throws -&amp;gt; PageData {
        let url = URL(string: &quot;http://127.0.0.1:8080/community/page/get&quot;)!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(PageData.self, from: data)
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;wrap的时候可以通过Task来执行它&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    func load() {
        Task{
            let newPageData = try await fetchPage()
            ···
            ······
            ·········
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的这是Swift6之后现代版的Post Call&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    func createTopic(title: String, content: String, time: Int64) async throws -&amp;gt; Topic {
        
        let newTopic = Topic(
            id: nil,
            title: title,
            content: content,
            create_time: time
        )
        
        guard let url = URL(string: &quot;http://127.0.0.1:8080/community/page/addtopic&quot;) else {
            throw URLError(.badURL)
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = &quot;POST&quot;
        request.setValue(&quot;application/json&quot;, forHTTPHeaderField: &quot;Content-Type&quot;)
        
        request.httpBody = try JSONEncoder().encode(newTopic)
        
        let (data, response) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              200..&amp;lt;300 ~= httpResponse.statusCode else {
            throw URLError(.badServerResponse)
        }
        
        return try JSONDecoder().decode(Topic.self, from: data)
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;wrap之后用Task执行也是和get同理&lt;/p&gt;
</content:encoded></item><item><title>在SwiftUI中实现MVVM模式</title><link>https://www.modo.org.cn/posts/%E5%9C%A8swiftui%E4%B8%AD%E5%AE%9E%E7%8E%B0mvvm%E6%A8%A1%E5%BC%8F/%E5%9C%A8swiftui%E4%B8%AD%E5%AE%9E%E7%8E%B0mvvm%E6%A8%A1%E5%BC%8F/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/%E5%9C%A8swiftui%E4%B8%AD%E5%AE%9E%E7%8E%B0mvvm%E6%A8%A1%E5%BC%8F/%E5%9C%A8swiftui%E4%B8%AD%E5%AE%9E%E7%8E%B0mvvm%E6%A8%A1%E5%BC%8F/</guid><description>用Swift写了一个从后端获取数据来展示话题和评论的客户端, 顺便学用了MVVM模式</description><pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;用Swift写了一个从后端获取数据来展示话题和评论的客户端, 顺便学用了MVVM模式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;MVVM即Model、View和ViewModel&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;View层:
&lt;strong&gt;UI界面，对的，记住它就仅仅是个UI&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;ViewModel层:
&lt;strong&gt;View要用到的所有数据和方法，嗯，data和function，所以它在Swift中通常是个class&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Mode层l:
&lt;strong&gt;底层数据和业务逻辑，在这个例子中包括向后端发出请求的底层方法，以及对应得到的json数据转换为的结构体&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;用ViewModel是为了视图和业务逻辑之间的解耦&lt;/h3&gt;
&lt;p&gt;从我的项目文件结构中应该能更加清晰说明这个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-CommentSystem
        
    -PageModel.swift
            
        -PageService.swift
            
        -PageData.swift
            
    -PageViewModel.swift
            
    -PageView.swift
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&quot; &lt;strong&gt;the View “knows” about the VM, but the VM knows nothing of the View. This is the blindest date ever&lt;/strong&gt; &quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;MVVM实现的是单向数据流，Model提供接口给到ViewModel,再通过ViewModel提供接口给到View&lt;/p&gt;
&lt;h3&gt;Swift中的实现&lt;/h3&gt;
&lt;p&gt;Model是底层上通用的一些数据和业务逻辑，自然就不用多提了&lt;/p&gt;
&lt;h4&gt;重点是ViewModel:&lt;/h4&gt;
&lt;p&gt;以下为这个项目的实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import SwiftUI
import Combine

public class PageViewModel: ObservableObject {
    @Published private(set) var title: String = &quot;&quot;
    @Published private(set) var contents: String = &quot;&quot;
    @Published private(set) var posts: [Post] = []
    @Published private(set) var errorMessage: String?
    @Published private(set) var cteateTime: Int64 = 0
    
    private let service: PageServiceProtocol
    
    init(service: PageServiceProtocol) {
        self.service = service
    }
    
    func loadPageById(input: Int) {
        Task{
            await load(pageId: input)
        }
    }
    
    func loadPageByTopic(input: String){
        Task{
            await query(title: input)
        }
    }
    
    private func query(title: String) async {
        do {
            let idData = try await service.fetchTopicId(title: title)
            await load(pageId: idData.id)
        } catch {
            self.errorMessage = error.localizedDescription
        }
    }
    
    private func load(pageId: Int) async {
        do {
            let pageData = try await service.fetchPage(Id: pageId)
            
            self.title = pageData.data.Topic.title
            self.posts = pageData.data.PostList
            self.cteateTime = pageData.data.Topic.create_time
            self.contents = pageData.data.Topic.content
            
        } catch {
            self.errorMessage = error.localizedDescription
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们把ViewModel直接作为一个类，
并使用ObservableObject*来定义它，待会儿在View中就可以用ObservedObject来监听它的ViewModel，@Published是你要向上开放的字段，也就是View要访问的字段，而&lt;strong&gt;func loadPageById&lt;/strong&gt;和&lt;strong&gt;func loadPageByTopic&lt;/strong&gt;则是View要使用的逻辑方法&lt;/p&gt;
&lt;p&gt;在这段中可以注意到的一点是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;init(service: PageServiceProtocol) {
        self.service = service
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里用到的service就是由Model提供上来的接口，viewModel中的所有字段和方法均由Model提供的数据和方法来实现&lt;/p&gt;
&lt;h4&gt;接下来是View去调用&lt;/h4&gt;
&lt;p&gt;原项目ui代码太多太杂了，以下为一个简化过了的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import SwiftUI

public struct ContentView: View {
    
    @ObservedObject var viewModel: PageViewModel
    @State private var input = &quot;&quot;
    
    public var body: some View {
        VStack {
            Text(viewModel.title)
                .font(.title)
            if viewModel.contents.isEmpty {
                Text(&quot;Loading...&quot;)
            } else {
                Text(viewModel.contents)
            }
            HStack {
                Button(&quot;Home&quot;) { viewModel.loadPageById(input: 1) }
                Button(&quot;教程&quot;) { viewModel.loadPageById(input: 3) }
                Button(&quot;赞助Modo&quot;) { viewModel.loadPageById(input: 2) }
            }
            TextField(&quot;Search&quot;, text: $input)
                .textFieldStyle(.roundedBorder)
                .onSubmit {
                    viewModel.loadPageByTopic(input: input)
                }
            
            List(viewModel.posts, id: \.content) { post in
                if let url = URL(string: post.content),
                   url.scheme?.hasPrefix(&quot;http&quot;) == true {
                    Link(post.content, destination: url)
                } else {
                    Text(post.content)
                }
            }
        }
        .padding()
        .onAppear {
            viewModel.loadPageById(input: 1)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过@ObservedObject监听ObservableObject即ViewModel的变化
当@Published变量改变时刷新View
public var body: some View{···}中的内容为具体的界面ui，可以看到全都采用ViewModel实例提供的数据和方法来实现&lt;/p&gt;
&lt;p&gt;最终ui界面如下：
&lt;img src=&quot;./ui.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>后端：文档管理项目</title><link>https://www.modo.org.cn/posts/%E5%90%8E%E7%AB%AF%E6%96%87%E6%A1%A3%E7%AE%A1%E7%90%86%E5%B0%8F%E9%A1%B9%E7%9B%AE/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/%E5%90%8E%E7%AB%AF%E6%96%87%E6%A1%A3%E7%AE%A1%E7%90%86%E5%B0%8F%E9%A1%B9%E7%9B%AE/</guid><description>写这个项目的时候本来用来理清需求和思路， 后来稍微添了点就顺便出来一份规范点文档</description><pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写这个项目的时候本来用来理清需求和思路， 后来稍微添了点就顺便出来一份规范点的文档&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;存储技术文档后端项目：&lt;/h1&gt;
&lt;h3&gt;你可能需要的文档：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;补充后端基础知识
https://youtu.be/XBu54nfzxAQ?si=0Ce6-dFM4nokSlwb&lt;/li&gt;
&lt;li&gt;（JWT入门）Spring Boot 如何集成JWT实现Token验证https://cloud.tencent.com/developer/article/2245008&lt;/li&gt;
&lt;li&gt;Spring Boot 零基础快速入门&lt;br /&gt;
https://kucw.io/blog/springboot/1/
&lt;ul&gt;
&lt;li&gt;Spring Boot Restful API介绍
https://kucw.io/blog/springboot/22/&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;spring与数据库的交互方式
https://dimitri.codes/difference-spring-data-jdbc-jpa/&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;p&gt;系统需求: 支持 Markdown 文件的上传、元数据存储以及权限化的访问管理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户与权限: 支持基础的JWT认证, 角色权限可以分成User/Admin&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文档管理:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户可以上传 .md 文件:
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;文件大小限制在 5mb&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支持分页查询&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支持通过文件名模糊搜索, 查询返回文档元数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后端需要读取 Markdown 的前 200 个字符作为“摘要”存入数据库字段。如果内容少于 10 个字符，视为无效文档，拒绝上传。此处考虑事务一致性&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;技术栈：&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;（maven）
Spring Boot + Spring Data JPA + PostgreSQL&lt;/p&gt;
&lt;h3&gt;My solutions:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;文档内容存文件，元数据存数据库&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;My steps:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;实现上传md文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件blobs与元数据commits分离&lt;/li&gt;
&lt;li&gt;将commit类接入数据库表&lt;/li&gt;
&lt;li&gt;upload接口实现：
&lt;ul&gt;
&lt;li&gt;文件blob写入到后端文件夹&lt;/li&gt;
&lt;li&gt;创建文件对应的元数据对象commit，然后把它写入数据库&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现JWT认证：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引入JWT依赖&lt;/li&gt;
&lt;li&gt;生成Token&lt;/li&gt;
&lt;li&gt;创建拦截器:解析验证Token
(拦截器创建之后，需要将拦截器注册到Spring Boot中)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现用户系统：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将User类接入数据库表&lt;/li&gt;
&lt;li&gt;实现account接口：
&lt;ol&gt;
&lt;li&gt;登录接口：接受username, password参数, 比对数据库中的&lt;/li&gt;
&lt;li&gt;注册接口：创建user类并写入数据库&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Others:
&lt;ul&gt;
&lt;li&gt;模糊搜索的实现使用Spring Data JPA自带关键词查询方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;notes!&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;User类接入数据库表时不要用 user 当表名，user 为PostgreSQL 的关键字，替换表名使用@Table(name = &quot;users&quot;)&lt;/li&gt;
&lt;li&gt;对于Spring Data JPA自带的Repository方法名，必须和实体字段名 一致（区分大小写） ex: Spring Data JPA自带关键词查询方法&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>GitletStop</title><link>https://www.modo.org.cn/posts/gitletstop/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/gitletstop/</guid><pubDate>Thu, 29 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;半夜想了一些还是决定先把61b放一会，现在看自己写的gitlet，实在有点在应付的感觉。主要手头上因为有新项目可以写，导致有一种 “拼命催自己写gitlet然后只要写完就可以写新的东西” 的感觉（虽然我也不知道为什么自己下意识先必须要完成这个）。回过来看了眼我的gitlet充满了赶工的痕迹，多少有点动机不纯，有点在逼着自己写，总之这种状态不是很健康，而且也怕浪费gitlet这个评价极高的项目。毕竟有新的东西更想学想做，那61b就暂且停一会吧。不管怎么说前一半的61b至少也已经把最核心的oop、 ADT思想交代清楚了&lt;/p&gt;
</content:encoded></item><item><title>GitletDay0</title><link>https://www.modo.org.cn/posts/gitletday0/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/gitletday0/</guid><pubDate>Wed, 28 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;为防止init_commit的重复创建，应把init_commit写入.gitlet，一次创建，其他地方直接引用&lt;/li&gt;
&lt;li&gt;sha接受byte数组，需要readContents先转化为 byte[]&lt;/li&gt;
&lt;li&gt;暂存区分tobeadded和tobeDeleted两种&lt;/li&gt;
&lt;li&gt;获取timeStamp:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Date now = new Date();

Date inittime = new Date(0);//the init time &apos;Thursday, 1 January 1970&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转换得到&lt;code&gt;String&lt;/code&gt;类型的timeStamp&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static String dateToTimeStamp(Date date) {
        DateFormat dateFormat = new SimpleDateFormat(&quot;EEE MMM d HH:mm:ss yyyy Z&quot;, Locale.US);
        return dateFormat.format(date);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;对于每个commit，其hashID通常这样得到&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sha1(Map.toString(), getParentCommit(), getMessage(), getTime());

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而非通过把commit暂存为序列化后的文件来计算hash, 可能java序列化不太稳定？
反正不适合用来算hash&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于过度依赖序列化导致我把很多指针直接写成文件来存储，这是错误的&lt;/li&gt;
&lt;li&gt;考虑到后面肯定是要调试的，临时写的这一坨明天得重构一下，这个项目骨架还是给太多了，如果不是因为后面有gradescope, 那我会考虑把这些骨架全都删了，就留个Utils和main，&lt;/li&gt;
&lt;li&gt;看了别人写gitlet的感受之后感觉有点不对，我应该把他当成一个自由的项目来写，而不是边写边一字一句看文档，毕竟gitlet长什么样不用看文档也足够清楚&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>雪？</title><link>https://www.modo.org.cn/posts/%E9%9B%AA/%E9%9B%AA/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/%E9%9B%AA/%E9%9B%AA/</guid><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;好不容易熬完期末周，不想睡觉&lt;/p&gt;
&lt;p&gt;Capoo半夜突然出阳台来看雪
于是我出阳台来看Capoo
冷🥶&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E9%9B%AA.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>从FSM进阶到HFSM</title><link>https://www.modo.org.cn/posts/%E4%BB%8Efsm%E8%BF%9B%E9%98%B6%E5%88%B0hfsm/%E4%BB%8Efsm%E8%BF%9B%E9%98%B6%E5%88%B0hfsm/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/%E4%BB%8Efsm%E8%BF%9B%E9%98%B6%E5%88%B0hfsm/%E4%BB%8Efsm%E8%BF%9B%E9%98%B6%E5%88%B0hfsm/</guid><description>祝生物钟倒转的三天新年快乐🎆</description><pubDate>Sun, 04 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;祝生物钟倒转的三天新年快乐🎆&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;基于上次的状态机：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./pasted-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;之前说过我当时写出这个状态机时，是从最初的if-else重构过来的。&lt;/p&gt;
&lt;p&gt;所以当时新接触这个状态机时，我也尝试过从if-else的角度去反向理解这个教简单的状态机，然后误打误撞就发现这其实就是分层状态机（HFSM）的思想&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./pasted-2.png&quot; alt=&quot;从if-else角度看状态机&quot; /&gt;&lt;/p&gt;
&lt;p&gt;事实上把陌生和友好两个状态合并起来就是对应着本的&apos;false&apos;,而逃跑状态则对应原本的&apos;true&apos;&lt;/p&gt;
&lt;p&gt;或者换句话说： &lt;strong&gt;if-else本身就是一种广义上的二元状态机&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;同时，在我们的这个例子中，我们用抽象的思维看，其实&apos;陌生和友好&apos;本身也是一个二元的状态机，它作为一个状态机被封装成了大的if-else二元状态机下的一个状态。&lt;/p&gt;
&lt;p&gt;也就是说：
&lt;strong&gt;状态机中的状态可以是另一个状态机&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这也就是分层状态机HFSM&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们把之前的FSM重构为HFSM,它总体上大致长这样
&lt;img src=&quot;./pasted-3.png&quot; alt=&quot;粗略的HFSM&quot; /&gt;&lt;/p&gt;
&lt;p&gt;hfsm的运行逻辑是先进入父状态机，然后根据过渡条件选择对应的子状态机，再运行子状态机并进入子状态机下的状态。&lt;/p&gt;
&lt;p&gt;各层级内需要做好抽象屏障，子状态机的状态不应与父状态机的状态存在任何通信。&lt;/p&gt;
&lt;p&gt;以下是HFSM的一个通用模型&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./pasted-4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于我们这个例子：&lt;/p&gt;
&lt;p&gt;父状态机是‘逃跑-不逃跑’，存在的子状态机是‘陌生-友好’。&lt;/p&gt;
&lt;p&gt;在由原先的FSM重构为HFSM的过程中，
状态之间原本都是直接通信
为了防止破坏层级抽象
实际上对于这个例子具体实现上需要一点特殊处理&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./pasted-5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;即通过外部的一个中转Boolean来让‘不逃跑’跳转到‘逃跑’,而不是直接从‘友好’或者‘陌生’跳转到‘逃跑’。&lt;/p&gt;
&lt;p&gt;当然,需要这样去操作也说明在构建hfsm时‘友好’和‘陌生’就不适合被分到同一状态机下，而且这个例子其实也没必要使用HFSM，只是为了从FSM引出HFSM而已。&lt;/p&gt;
&lt;p&gt;HFSM真正的适用场景是在FSM的状态数量已经达到非常多时，难以管理，遂把各个小状态依据关联性分类并组装成一个个子状态机，然后嵌入到父状态机中，可以进行很多层的封装。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码实现可以参考https://zhuanlan.zhihu.com/p/558422986&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>关于FSM有限状态机</title><link>https://www.modo.org.cn/posts/%E5%85%B3%E4%BA%8Efsm%E7%8A%B6%E6%80%81%E6%9C%BA/%E5%85%B3%E4%BA%8Efsm%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA/</link><guid isPermaLink="true">https://www.modo.org.cn/posts/%E5%85%B3%E4%BA%8Efsm%E7%8A%B6%E6%80%81%E6%9C%BA/%E5%85%B3%E4%BA%8Efsm%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA/</guid><pubDate>Mon, 01 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;本篇基于Modo在写&amp;lt;a href=&quot;https://modrinth.com/mod/flee-on-sight&quot;&amp;gt;&lt;em&gt;Flee On Sight&lt;/em&gt;&amp;lt;/a&amp;gt;时的感想&lt;/h1&gt;
&lt;h3&gt;对于较复杂的状态判断，有一种更好的方法&lt;/h3&gt;
&lt;h4&gt;为什么不用if-else ?:&lt;/h4&gt;
&lt;p&gt;首先，对于大量子状态的判定会在这种boolean的判定中越来越冗长，如果是写了辅助方法来包装则会有层层嵌套的问题，而且对于几个不相关的子boolean强行包装成一个辅助boolean会导致可读性下降，最终整体的状态判定会很难维护&lt;/p&gt;
&lt;p&gt;同时，最最危险的一点在于if-else的各种子状态判定一旦存在耦合，会变得非常非常麻烦
举个例子，我们判断一只羊是否应该逃跑（记为isFleeing）, 假设isFleeing这个boolean由两个子状态A和B来判定：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A 代表空间判定（包含视角判定，空间距离判定）

B 代表食物引诱判定（即羊不应在受吸引时逃跑）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或许这个时候你会想这么写&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (A &amp;amp;&amp;amp; B) {
    isFleeing = true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而事实是B中实际上嵌套了A中的部分空间逻辑🤓，这意味着 A 与 B 并不是两个独立条件，这就非常头疼了&lt;/p&gt;
&lt;p&gt;通常接下来你有两种常规的处理这玩意的思路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;第一种是打补丁：在A中嵌套B联合一些其他的空间状态判定，从而在耦合的那块地方强行用B覆盖A(假如决策系统稍微再复杂一点点甚至会需要A与B互相嵌套)。
最终A与B之间的耦合度越来越高导致会严重破坏抽象，可以想象调试这个逻辑时在两个子状态判定之间反复横跳的场景。

第二种是强行拆散耦合，直接不要这两个较大的子状态，把他们拆散成一个个小的boolean判定。
但这就没有封装了，可读性就不要想了，面对一堆boolean维护起来也是极其困难。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;那么 ，该到我们的FSM有限状态机登场了！&lt;/h3&gt;
&lt;p&gt;Modo被羊的if-else折磨得死来活去时，突然发现了有限状态机FSM这个好东西&lt;/p&gt;
&lt;h4&gt;从羊的角度切入：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;    有限状态机相当于把羊赋予一个’状态‘，状态可以有有限个种类

    但是一只羊同时有且仅有一个状态

    每个状态之间有精确的进入和退出条件

    我们把羊的状态分为如下三个种类：陌生，逃跑，友好
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./FSM.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们现在先根据之前我们想要通过if-else达到的目的效果来想想各个状态是什么意思：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    陌生：
        对玩家存在警觉，但不是逃跑
    逃跑：
        字面意义，即处于逃跑状态
    友好：
        对玩家毫无防备
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们根据各个状态间的关系添加进入/退出条件即可
（注意，这些是进入/退出条件，也就是说状态是可以持续的，不改变的）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./fsm2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    每个tick中，羊仅处于一种状态中，

    只有当状态为逃跑时，羊才会调用逃跑方法
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下为具体代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public enum State {
    DEFAULT_EMPTY,
    FLEEING,
    FRIENDLY
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;switch (mobState.currentState){
            case DEFAULT_EMPTY:
                if(FOVcheck(animal, player) &amp;amp;&amp;amp; player.isHolding(Items.WHEAT) &amp;amp;&amp;amp; distance &amp;lt;= 8.2){
                    mobState.currentState = FRIENDLY;
                }
                else if(distance &amp;lt;= 8 &amp;amp;&amp;amp; FOVcheck(animal, player)){
                    mobState.currentState = FLEEING;
                }
                break;

            case FRIENDLY:
                if(animal.getAttacker() == player){
                    mobState.currentState = FLEEING;
                }
                break;

            case FLEEING:
                 if(distance &amp;gt;= 20){
                    mobState.currentState = DEFAULT_EMPTY;
                }
                break;
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们当然也可以用if-else去做到这件事，
但是比起用if-else系统，状态机的可读性和可维护性会好很多。&lt;/p&gt;
&lt;p&gt;实际上上述提到的这个羊的行为逻辑只是较规模小的一点实现，一旦决策系统更加庞大之后，状态机的优势就会更好地体现出来&lt;/p&gt;
&lt;p&gt;原因在于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 我们每时每刻只处于一个状态中

- 我们只要管好局部每个状态时的进入和退出条件即可，可以忽略其他状态之间的交互细节，极大程度上降低了耦合度
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;杂谈：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;上述状态机的具体实现只是其中的一种简单形式， 在Java中即是使用enum配合switch来构成状态机，对于更重型的状态机，有更严谨和标准的方式通过接口来实现（一个真正意义上完整的状态机包含：状态，转换，和执行，其中执行可以在特定状态下也可以在转换时）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;状态机可以应付复杂的状态判定中，所以被大量运用在游戏ai逻辑处理中， 包括你可以在mc的反编译源码中找到它&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;推荐一篇极好的状态机讲解 https://youtu.be/-ZP2Xm-mY4E?si=Qg5BXm2E3TQxlltM&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>