[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-48":3},{"id":4,"title":5,"title_en":6,"abstract":7,"abstract_en":8,"content":9,"content_en":10,"category":11,"banner_id":12,"banner_path":13,"tags":14,"is_recommend":17,"prev_article":18,"next_article":22,"created_at":26},48,"我是如何用 Go 语言造出一个\"虚拟世界\"的——从死锁地狱到事件驱动架构","How I Built a \"Virtual World\" in Go: From Deadlock Hell to Event-Driven Architecture","> **前言**：这篇文章不要求你有多深的技术背景。如果你知道\"Go 语言是什么\"，能看懂一点点代码","his article does not require you to have a deep technical background. If you know \"what the Go language is\" and can understand a tiny bit of code, you will be able to finish reading this article and figure out how the underlying layer of a complex system actually runs. If you have never heard of Go, that's","> **前言**：这篇文章不要求你有多深的技术背景。如果你知道\"Go 语言是什么\"，能看懂一点点代码，那你就能看完这篇文章，并且搞清楚一个复杂系统的底层到底是怎么跑起来的。如果你没听过 Go 也没关系，文章里所有的技术概念我都会用生活里的比喻来解释。\n\n---\n\n在之前的文章里，我介绍了自己写的一个图数据库库 `GopherGraph`，它解决的是\"如何存储和查询复杂关系网络\"的问题。\n\n但今天这篇，要解决一个更具挑战性的问题：**如何让这个世界里的成百上千个\"居民\"，同时动起来，还不互相打架？**\n\n---\n\n## 第一章：先聊聊我们在做什么\n\n想象一下你在做一款类似《模拟人生》的游戏，或者一个拥有众多 NPC 的开放世界。世界里有 1000 个居民，他们每个人都有自己的行为逻辑：\n\n- 小王今天去市场买菜\n- 小李在河边钓鱼\n- 小赵在酒馆和人聊天，刚好遇到了小王\n\n他们**同时**在世界里活动，互相影响，互相交互。\n\n现在问题来了：**你的程序，要怎么同时运行这 1000 个人的行为逻辑？**\n\n这就是我们今天要解决的核心问题，它的专业叫法是：**高并发实体调度**。\n\n---\n\n## 第二章：最直觉的做法，以及为什么它会崩\n\n### 2.1 最朴素的想法：一个大循环\n\n刚开始写原型的时候，我们用的是最简单粗暴的方式：\n\n```go\nfor _, entity := range allEntities {\n    entity.Update() \u002F\u002F 逐个更新每个实体的状态\n}\n```\n\n这就像一个老师，挨个叫每个同学\"报一下你今天干了什么\"。\n\n这种方式有个致命缺陷：**串行执行**。1000 个居民，第 1 个更新完了才轮到第 2 个。这 1000 个人的行为在程序里其实是一个接一个发生的，根本不是\"同时\"，只是模拟出来的假象。\n\n当实体数量多起来，或者每个实体的逻辑变复杂，整个世界的\"时钟\"就会越来越慢，最终卡成 PPT。\n\n### 2.2 聪明一点：给每个居民开一个线程\n\n既然串行不行，那就并行。对于每个实体，我们都开一个 **Goroutine**（Go 语言里的轻量级线程，可以理解成一个独立的执行通道）让他们真正同时跑起来。\n\n```go\nfor _, entity := range allEntities {\n    go entity.Update() \u002F\u002F 并发！每个居民同时执行\n}\n```\n\n看起来很美，但新的问题出现了。\n\n### 2.3 并发带来的新灾难：死锁\n\n小王去市场买菜，需要访问\"市场\"这个共享资源。同时，小李也去市场卖鱼，同样需要访问\"市场\"。\n\n在程序里，这个\"市场\"就是一段共享内存（Shared Memory）。当两个 Goroutine 同时读写同一块数据时，会出现**数据竞争（Race Condition）**，程序结果就完全不可预测了。\n\n为了防止这个问题，我们给每个共享资源加了一把锁（Mutex）：\n\n> \"我在用市场，你们都等着，我用完了你们才能用。\"\n\n但加锁就会带来新问题——**死锁（Deadlock）**。\n\n我用生活来解释死锁：\n\n> 小王说：\"我要买鱼，但我要先去仓库拿钱，钱拿到了才能去市场买。\"\n> 小李说：\"我要存鱼，但我要先去市场确认有空位，有空位了才去仓库取筐。\"\n>\n> 于是：\n> - 小王锁住了仓库，等市场\n> - 小李锁住了市场，等仓库\n>\n> **两个人都在等对方，谁也走不了，程序就这样卡死了。**\n\n这不是玩笑，我们的早期版本确实出现了这种情况，而且调试的过程非常痛苦——死锁不会报错，程序只是静悄悄地\"假死\"，你根本不知道它卡在哪里。\n\n除了死锁，还有一个问题是**锁竞争（Lock Contention）**：当 500 个居民同时要访问同一个区域，而这个区域只有一把锁，500 个人就得排队，并发的优势完全被抵消了。\n\n这就是为什么我们需要一种全新的架构思路。\n\n---\n\n## 第三章：Go 语言的哲学，以及它如何改变了我们的思路\n\nGo 语言的设计者有一句广为人知的名言：\n\n> **\"不要通过共享内存来通信，而要通过通信来共享内存。\"**\n> ——Rob Pike\n\n这句话听起来有点绕，翻译成人话就是：\n\n> **不要让大家去抢同一个东西，而是让大家互相\"发消息\"。**\n\n这就是我们后来整个架构的底层哲学。\n\n---\n\n## 第四章：Actor 模型——给每个居民一个\"专属信箱\"\n\n新的架构参考了软件工程领域一个经典的并发模型：**Actor 模型**。\n\n**Actor 模型的核心思想只有三条：**\n\n1. 每个 Actor（比如一个居民）都是完全独立的，有自己的私有状态。\n2. Actor 之间绝对不能直接读写对方的状态（不能随便进对方家里翻东西）。\n3. Actor 之间的唯一沟通方式，是给对方**发消息**（往对方信箱里塞一张纸条）。\n\n每个居民的信箱，在 Go 里用 **Channel（管道）** 来实现。Channel 是 Go 语言原生支持的并发通信机制，天生线程安全，不需要加锁。\n\n我们重新设计了每个实体（居民）的内部结构：\n\n```go\ntype Entity struct {\n    id      string\n    mailbox chan *Event    \u002F\u002F 私有信箱（带缓冲的 Channel）\n    state   *EntityState  \u002F\u002F 私有状态，只有自己能改\n}\n```\n\n每个实体都有一个自己专属的 `mailbox`，这是一个 Channel，外界只能往里面\"投消息\"，不能直接操作 `state`。\n\n然后，每个实体都有一个自己的\"生命循环\"：\n\n```go\nfunc (e *Entity) Start(ctx context.Context) {\n    \u002F\u002F 给每个实体开一个专属 Goroutine，让它独立运作\n    go func() {\n        ticker := time.NewTicker(100 * time.Millisecond) \u002F\u002F 每 100ms 自主行动一次\n        defer ticker.Stop()\n\n        for {\n            select {\n\n            case \u003C-ctx.Done():\n                \u002F\u002F 世界结束了，或者这个居民\"死亡\"了，优雅地退出\n                e.cleanup()\n                return\n\n            case event := \u003C-e.mailbox:\n                \u002F\u002F 有人给我发消息了！处理它\n                \u002F\u002F 注意：这里修改 state 完全不需要加锁，因为只有这一个 Goroutine 在写\n                e.handleEvent(event)\n\n            case \u003C-ticker.C:\n                \u002F\u002F 主动触发：时间到了，做自己该做的事（比如走路、说话）\n                e.doAction()\n            }\n        }\n    }()\n}\n```\n\n这段代码里，最关键的是 `select` 语句，它就像一个\"多路监听器\"，同时监听三件事：\n\n- **有人给我发消息**：优先处理\n- **时间滴答到了**：主动触发自己的行为\n- **系统通知我退出**：干净地结束自己\n\n由于每个实体的 `state` 只有它自己的那个 Goroutine 才能修改，我们**彻底告别了 Mutex，告别了死锁**。\n\n---\n\n## 第五章：事件总线——虚拟世界的\"邮政系统\"\n\n现在居民们都有了自己的信箱，但谁来送信？\n\n这就是**事件总线（Event Bus）**登场的时候了。\n\n### 5.1 什么是事件？\n\n在我们的系统里，任何一个\"发生的事情\"都被抽象成一个**事件（Event）**。\n\n比如：\n\n| 事件类型 | 发送方 | 接收方 | 内容 |\n|----------|--------|--------|------|\n| 移动 | 小王 | 系统 | \"我要去 (10, 20) 坐标\" |\n| 对话 | 小李 | 小王 | \"今天天气不错啊\" |\n| 交易 | 小王 | 小李 | \"我想买你的鱼\" |\n| 区域广播 | 系统 | 所有在 A 区域的居民 | \"A 区域开始下雨了\" |\n\n每个事件都是一个干净的数据结构：\n\n```go\ntype Event struct {\n    Type     EventType  \u002F\u002F 事件的类型（移动？对话？交易？）\n    SourceID string     \u002F\u002F 谁发出的\n    TargetID string     \u002F\u002F 发给谁（如果是广播，则为 Topic 名称）\n    Payload  any        \u002F\u002F 事件携带的具体数据\n    Priority int        \u002F\u002F 优先级（高优先级的事件优先处理）\n}\n```\n\n### 5.2 事件总线是怎么工作的？\n\n你可以把整个事件总线想象成现实世界里的**快递系统**：\n\n1. 小王（Entity A）不能直接闯进小李的家里（不能直接修改对方状态）\n2. 小王填写一张\"快递单\"（创建一个 Event），告诉系统：要发给谁、发什么\n3. 小王把快递单扔给快递公司（Event Bus）\n4. 快递公司按照地址，把包裹送到小李的信箱（mailbox Channel）\n5. 小李在自己的时间里打开信箱，处理这个事件\n\n这个过程是**完全异步**的：小王不需要等小李处理完才能继续自己的事。就像你在网上下了订单，不需要盯着快递员，他总会送到的。\n\n### 5.3 事件总线的内部结构\n\n我们的事件总线内部，是一个多 Worker 的并发分发器：\n\n```go\ntype EventBus struct {\n    workers    int                      \u002F\u002F Worker 的数量\n    queue      chan *Event              \u002F\u002F 全局事件队列\n    registry   map[string]*Entity      \u002F\u002F ID -> 实体的注册表\n}\n\nfunc (bus *EventBus) Publish(event *Event) {\n    \u002F\u002F 非阻塞地把事件放入队列\n    \u002F\u002F 如果队列满了，根据策略决定：丢弃？重试？还是报警？\n    select {\n    case bus.queue \u003C- event:\n        \u002F\u002F 成功入队\n    default:\n        \u002F\u002F 队列已满，触发降级策略\n        bus.handleOverflow(event)\n    }\n}\n\nfunc (bus *EventBus) startWorkers() {\n    for i := 0; i \u003C bus.workers; i++ {\n        go func() {\n            for event := range bus.queue {\n                \u002F\u002F 从注册表里找到目标实体\n                if target, ok := bus.registry[event.TargetID]; ok {\n                    \u002F\u002F 投递到实体的信箱\n                    \u002F\u002F 同样非阻塞，防止一个慢实体拖垮整个系统\n                    select {\n                    case target.mailbox \u003C- event:\n                    default:\n                        \u002F\u002F 实体的信箱也满了，根据事件优先级决定怎么处理\n                        bus.handleEntityOverflow(target, event)\n                    }\n                }\n            }\n        }()\n    }\n}\n```\n\n注意代码里反复出现的 `select { case ...: default: }` 这个模式——这是 Go 语言里实现**非阻塞 Channel 操作**的标准写法。它的意思是：如果 Channel 现在能接收就立即发送，如果不能接收就走 `default` 分支做其他处理，**绝不死等**。这是防止级联阻塞、保证系统健壮性的关键。\n\n---\n\n## 第六章：如何避免系统被\"踩踏事故\"压垮\n\n引入事件总线之后，我们很快遇到了新的挑战：**事件洪峰（Event Flood）**。\n\n想象一个场景：某一个区域突然开始下暴雨，系统需要通知这个区域里的所有 500 个居民。如果我们瞬间向 500 个 Channel 写入数据，会发生什么？\n\n每个居民的信箱（缓冲 Channel）可能装不下，系统瞬间被海量的写操作压垮，响应延迟急剧升高，严重时甚至导致整个系统雪崩。\n\n### 6.1 区分\"重要消息\"和\"通知消息\"\n\n现实中的快递系统也有类似的分层：顺丰当日达、普通快递3天到、不重要的广告信可以扔垃圾桶。\n\n我们对事件做了分类：\n\n- **可靠事件（Reliable Event）**：比如\"交易确认\"、\"关键状态变更\"。这些不能丢，如果信箱满了，就放入**重试队列**，等一段时间再重新投递。\n- **尽力事件（Lossy Event）**：比如\"背景音效通知\"、\"普通移动广播\"。这些即使丢了也无所谓，信箱满了直接丢弃，保护系统整体的稳定性。\n\n这种思路在分布式系统里被称为**背压（Backpressure）控制**，本质上是在系统过载时，主动丢弃不那么重要的工作，保证核心功能正常运转。\n\n### 6.2 AOI 算法——只通知\"附近的人\"\n\n这是解决事件洪峰最优雅的方案，灵感来自游戏服务器开发领域的经典算法：**AOI（Area of Interest，感兴趣区域）**。\n\n核心思想超级简单：**如果一件事发生在 A 区域，就只通知 A 区域以及周围紧邻区域的人，远处的人不需要知道。**\n\n具体实现：把整个虚拟世界用一张网格（Grid）划分开来，每个格子维护一个\"当前在这里的居民列表\"。当有广播事件时，只查询当前格子以及周围 8 个格子（九宫格）的居民，向他们投递事件。\n\n```\n+---+---+---+\n| ↖ | ↑ | ↗ |\n+---+---+---+\n| ← | ★ | → |   ★ = 事件发生地\n+---+---+---+\n| ↙ | ↓ | ↘ |\n+---+---+---+\n\n只通知这9个格子里的居民\n```\n\n这个小小的改变，把\"通知所有人\"的复杂度从 **O(N)**（N 是世界里所有居民总数）直接降到了近乎常数级——无论世界里有多少居民，广播的代价都基本不变。\n\n---\n\n## 第七章：动态世界与静态图谱的结合——闭环\n\n现在整个虚拟世界的运转逻辑已经清晰了：\n1. 每个居民是一个独立的 Actor，有私有状态和信箱\n2. 居民之间通过事件进行通信，不直接互相干涉\n3. Event Bus 负责高效分发，AOI 算法控制广播范围\n\n但还有一个问题：所有的状态都在内存里跑，那些复杂的**关系**怎么持久化？比如\"谁和谁是朋友\"、\"谁拥有哪块土地\"、\"谁曾经在哪里出现过\"——这些关系数据，正好是图数据库最擅长处理的。\n\n这就是之前那篇文章里介绍的 `GopherGraph` 的用武之地。\n\n我们采用了一种叫做**事件溯源（Event Sourcing）**的模式：事件总线在分发事件的同时，还会把所有关键事件异步地写入到 GopherGraph 中。\n\n```\n实时内存层（本文的引擎）\n      ↓ 事件流持续异步落盘\n持久图谱层（GopherGraph）\n```\n\n这样，内存层负责处理毫秒级的实时交互和状态更新，图谱层负责沉淀下来的关系数据，提供复杂的历史查询与数据分析能力。两层各司其职，互不干扰。\n\n---\n\n## 结语：这套架构解决了什么，牺牲了什么\n\n任何架构设计都有代价，我们最后来做一个诚实的总结：\n\n### ✅ 我们解决了：\n- **死锁问题**：Actor 模型 + Channel，彻底消灭了 Mutex，死锁风险降到极低\n- **性能瓶颈**：真正的并行执行，充分利用多核 CPU\n- **代码耦合**：事件驱动让每个模块彻底解耦，新增一种居民行为不需要改动任何已有代码\n- **可扩展性**：按区域拆分事件总线，理论上可以横向扩展到多台服务器\n\n### ⚠️ 我们付出的代价：\n- **调试复杂度变高**：异步事件的调用链很长，出了 bug 有时候很难追溯是哪一步出了问题。我们为此专门开发了一套事件日志追踪系统。\n- **心智负担增加**：开发者需要始终保持\"一切都是异步\"的思维，习惯从同步调用转过来，需要一定的学习成本。\n- **最终一致性**：由于是异步通信，某一时刻不同实体看到的世界状态可能略有差异，需要业务层面接受\"最终一致\"而非\"强一致\"。\n\n---\n\n**Go 语言为这套架构提供了天然的土壤**。几千个 Goroutine 对 Go 的 Runtime 来说轻描淡写，Channel 和 select 语法让并发通信写起来优雅自然，`context` 包让生命周期管理变得轻而易举。\n\n希望这篇文章能让你对并发系统的设计有新的认识。如果你对 Actor 模型、事件驱动、或者 GopherGraph 的具体实现有任何问题，欢迎在评论区留言，我们下期继续聊。\n","> **Preface**: This article does not require you to have a deep technical background. If you know \"what the Go language is\" and can understand a tiny bit of code, you will be able to finish reading this article and figure out how the underlying layer of a complex system actually runs. If you have never heard of Go, that's fine too; I will use real-life analogies to explain all the technical concepts in this article.\n\n---\n\nIn the previous article, I introduced `GopherGraph`, a graph database library I wrote. It resolves the problem of \"how to store and query complex relationship networks.\"\n\nBut today's article will tackle a more challenging question: **How do we make hundreds or thousands of \"residents\" in this world move at the same time without fighting each other?**\n\n---\n\n## Chapter 1: What Are We Doing?\n\nImagine you are making a game like *The Sims*, or an open world with many NPCs. There are 1,000 residents in the world, and each of them has their own behavioral logic:\n\n* Xiao Wang goes to the market to buy groceries today.\n* Xiao Li is fishing by the river.\n* Xiao Zhao is chatting with someone in the tavern and happens to run into Xiao Wang.\n\nThey are active in the world **at the same time**, influencing and interacting with each other.\n\nNow the question arises: **How does your program run the behavioral logic of these 1,000 people simultaneously?**\n\nThis is the core problem we are going to solve today, and its professional term is: **High-Concurrency Entity Scheduling**.\n\n---\n\n## Chapter 2: The Most Intuitive Approach, and Why It Collapses\n\n### 2.1 The Most Naive Idea: A Big Loop\n\nWhen we first started writing the prototype, we used the simplest and most brutal way:\n\n```go\nfor _, entity := range allEntities {\n    entity.Update() \u002F\u002F Update the state of each entity one by one\n}\n```\n\nThis is like a teacher calling on each student one by one to \"report what you did today.\"\n\nThis approach has a fatal flaw: **sequential execution**. With 1,000 residents, the first one must finish updating before the second one gets a turn. In reality, the actions of these 1,000 people occur one after another in the program—it's not \"simultaneous\" at all; it's just an illusion simulated by the CPU.\n\nAs the number of entities grows, or the logic of each entity becomes more complex, the \"clock\" of the entire world slows down, eventually lagging like a PowerPoint presentation.\n\n### 2.2 A Bit Smarter: Open a Thread for Each Resident\n\nSince sequential execution doesn't work, let's go parallel. For each entity, we spin up a **Goroutine** (a lightweight thread in Go, which can be understood as an independent execution channel) so they can truly run concurrently.\n\n```go\nfor _, entity := range allEntities {\n    go entity.Update() \u002F\u002F Concurrent! Every resident executes at the same time\n}\n```\n\nThis looks beautiful, but new problems arise.\n\n### 2.3 The New Disaster of Concurrency: Deadlocks\n\nXiao Wang goes to the market to buy groceries and needs to access the \"Market\" shared resource. At the same time, Xiao Li also goes to the market to sell fish, and also needs to access the \"Market.\"\n\nIn the program, this \"Market\" is a piece of shared memory. When two Goroutines read and write to the same data at the same time, a **race condition** occurs, making the program's results completely unpredictable.\n\nTo prevent this problem, we added a lock (Mutex) to each shared resource:\n\n> \"I am using the market. Everyone else wait. You can use it only after I'm done.\"\n\nBut locking brings a new problem—**Deadlock**.\n\nLet me explain deadlocks using a real-life analogy:\n\n> Xiao Wang says: \"I want to buy fish, but I need to go to the warehouse to get money first. I can only go to the market to buy fish after I get the money.\"\n>\n> Xiao Li says: \"I want to store fish, but I need to check the market for an empty slot first. I will only go to the warehouse to get a basket after I confirm there is an empty slot.\"\n>\n> As a result:\n> * Xiao Wang locks the warehouse and waits for the market.\n> * Xiao Li locks the market and waits for the warehouse.\n>\n> **Both are waiting for each other, and neither can move. The program gets stuck forever.**\n\nThis is not a joke; our early versions did encounter this situation, and the debugging process was extremely painful. A deadlock does not throw an error; the program just quietly goes \"braindead,\" and you have no idea where it is stuck.\n\nIn addition to deadlocks, there is also **Lock Contention**: when 500 residents want to access the same area at the same time, and this area has only one lock, 500 people have to queue up, completely offsetting the advantage of concurrency.\n\nThis is why we need a brand-new architectural approach.\n\n---\n\n## Chapter 3: Go's Philosophy, and How It Changed Our Thinking\n\nThe creators of Go have a widely known proverb:\n\n> **\"Do not communicate by sharing memory; instead, share memory by communicating.\"**\n> — Rob Pike\n\nThis sounds a bit abstract. Translated into plain English, it means:\n\n> **Do not let everyone fight for the same thing; instead, let everyone \"send messages\" to each other.**\n\nThis became the underlying philosophy of our subsequent architecture.\n\n---\n\n## Chapter 4: The Actor Model—Giving Each Resident a \"Dedicated Mailbox\"\n\nThe new architecture references a classic concurrency model in software engineering: the **Actor Model**.\n\n**The core ideas of the Actor Model are simple:**\n1. Each Actor (e.g., a resident) is completely independent and maintains its own private state.\n2. Actors must never directly read or write to another Actor's state (you can't just barge into someone else's house and rummage through their things).\n3. The only way Actors communicate is by **sending messages** (dropping a note into the other person's mailbox).\n\nIn Go, each resident's mailbox is implemented using a **Channel**. Channels are the native concurrent communication mechanism in Go. They are thread-safe by design and do not require manual locking.\n\nWe redesigned the internal structure of each entity (resident):\n\n```go\ntype Entity struct {\n    id      string\n    mailbox chan *Event    \u002F\u002F Private mailbox (buffered Channel)\n    state   *EntityState  \u002F\u002F Private state, modifiable only by oneself\n}\n```\n\nEach entity has its own dedicated `mailbox` Channel. The outside world can only \"post messages\" into it but cannot directly manipulate the `state`.\n\nThen, each entity has its own \"life cycle loop\":\n\n```go\nfunc (e *Entity) Start(ctx context.Context) {\n    \u002F\u002F Open a dedicated Goroutine for each entity so it operates independently\n    go func() {\n        ticker := time.NewTicker(100 * time.Millisecond) \u002F\u002F Autonomously act every 100ms\n        defer ticker.Stop()\n\n        for {\n            select {\n\n            case \u003C-ctx.Done():\n                \u002F\u002F The world has ended, or this resident has \"died\", exit gracefully\n                e.cleanup()\n                return\n\n            case event := \u003C-e.mailbox:\n                \u002F\u002F Someone sent me a message! Handle it.\n                \u002F\u002F Note: Modifying state here requires absolutely no locks,\n                \u002F\u002F because only this single Goroutine is writing to it.\n                e.handleEvent(event)\n\n            case \u003C-ticker.C:\n                \u002F\u002F Active trigger: time is up, do what needs to be done (e.g., walk, talk)\n                e.doAction()\n            }\n        }\n    }()\n}\n```\n\nIn this code, the most critical part is the `select` statement, which acts like a \"multi-way listener,\" listening to three things at once:\n* **有人给我发消息 (Someone sent me a message)**: Process it with priority.\n* **时间滴答到了 (The timer ticked)**: Actively trigger own behaviors.\n* **系统通知我退出 (The system notified me to exit)**: Clean up and terminate.\n\nSince each entity's `state` can only be modified by its own Goroutine, we have **completely said goodbye to Mutexes and deadlocks**.\n\n---\n\n## Chapter 5: The Event Bus—The \"Postal System\" of the Virtual World\n\nNow that the residents have their own mailboxes, who delivers the messages?\n\nThis is where the **Event Bus** comes in.\n\n### 5.1 What Is an Event?\n\nIn our system, anything that \"happens\" is abstracted as an **Event**.\n\nFor example:\n\n| Event Type | Sender | Receiver | Content |\n| :--- | :--- | :--- | :--- |\n| Move | Xiao Wang | System | \"I want to go to coordinates (10, 20)\" |\n| Talk | Xiao Li | Xiao Wang | \"The weather is nice today\" |\n| Trade | Xiao Wang | Xiao Li | \"I want to buy your fish\" |\n| Area Broadcast | System | All residents in Area A | \"It is starting to rain in Area A\" |\n\nEach event is a clean data structure:\n\n```go\ntype Event struct {\n    Type     EventType  \u002F\u002F Event type (Move? Talk? Trade?)\n    SourceID string     \u002F\u002F Who sent it\n    TargetID string     \u002F\u002F Who it is sent to (Topic name if it's a broadcast)\n    Payload  any        \u002F\u002F Specific data carried by the event\n    Priority int        \u002F\u002F Priority (high-priority events are processed first)\n}\n```\n\n### 5.2 How Does the Event Bus Work?\n\nYou can imagine the Event Bus as a **delivery system** in the real world:\n1. Xiao Wang (Entity A) cannot directly barge into Xiao Li's house (cannot directly modify the other's state).\n2. Xiao Wang fills out a \"delivery slip\" (creates an `Event`), specifying the recipient and the content.\n3. Xiao Wang drops the delivery slip off at the courier company (publishes to the `EventBus`).\n4. The courier company delivers the package to Xiao Li's mailbox (the `mailbox` Channel) according to the address.\n5. Xiao Li opens his mailbox and processes the event during his own execution time.\n\nThis process is **completely asynchronous**: Xiao Wang does not need to wait for Xiao Li to finish processing before continuing his own business. It's just like placing an order online; you don't need to watch the delivery guy the whole time—he will eventually get it there.\n\n### 5.3 Internal Structure of the Event Bus\n\nInside our Event Bus, there is a multi-worker concurrent dispatcher:\n\n```go\ntype EventBus struct {\n    workers    int                      \u002F\u002F Number of worker Goroutines\n    queue      chan *Event              \u002F\u002F Global event queue\n    registry   map[string]*Entity      \u002F\u002F ID -> Entity registration registry\n}\n\nfunc (bus *EventBus) Publish(event *Event) {\n    \u002F\u002F Non-blocking write to the queue\n    \u002F\u002F If the queue is full, decide the strategy: drop? retry? or alert?\n    select {\n    case bus.queue \u003C- event:\n        \u002F\u002F Successfully enqueued\n    default:\n        \u002F\u002F Queue is full, trigger degradation strategy\n        bus.handleOverflow(event)\n    }\n}\n\nfunc (bus *EventBus) startWorkers() {\n    for i := 0; i \u003C bus.workers; i++ {\n        go func() {\n            for event := range bus.queue {\n                \u002F\u002F Find the target entity in the registry\n                if target, ok := bus.registry[event.TargetID]; ok {\n                    \u002F\u002F Deliver to the entity's mailbox\n                    \u002F\u002F Also non-blocking to prevent a slow entity from bottlenecking the system\n                    select {\n                    case target.mailbox \u003C- event:\n                    default:\n                        \u002F\u002F The entity's mailbox is also full, handle based on event priority\n                        bus.handleEntityOverflow(target, event)\n                    }\n                }\n            }\n        }()\n    }\n}\n```\n\nNotice the recurring `select { case ...: default: }` pattern in the code—this is the standard Go idiom for **non-blocking Channel operations**. It means: if the Channel can receive now, send it immediately; if not, go to the `default` branch for alternative handling, **never wait indefinitely**. This is the key to preventing cascading blockage and ensuring system robustness.\n\n---\n\n## Chapter 6: Preventing System Collapse from \"Stampede Accidents\"\n\nAfter introducing the Event Bus, we quickly encountered a new challenge: **Event Floods**.\n\nImagine a scenario: it suddenly starts pouring rain in a certain area, and the system needs to notify all 500 residents in this area. If we write data to 500 Channels at the exact same moment, what will happen?\n\nEach resident's mailbox (buffered Channel) might overflow, and the system will instantly be overwhelmed by a massive wave of write operations, causing response latency to spike and, in severe cases, triggering a system-wide cascade failure.\n\n### 6.1 Distinguishing \"Important Messages\" from \"Notification Messages\"\n\nReal-world courier systems also have similar tiered routing: express delivery for same-day delivery, standard mail for 3-day delivery, and junk flyers that can be tossed straight into the bin.\n\nWe classified events into two tiers:\n* **Reliable Events**: e.g., \"Trade Confirmation\", \"Critical State Change\". These cannot be lost. If the mailbox is full, they are placed in a **retry queue** to be redelivered after a delay.\n* **Lossy Events**: e.g., \"Background Ambient Sound Notification\", \"Ordinary Move Broadcast\". It doesn't matter if these are lost. If the mailbox is full, they are discarded immediately to protect the overall stability of the system.\n\nThis concept is known as **Backpressure Control** in distributed systems. It essentially means actively discarding non-critical work when the system is overloaded to ensure core functions keep running.\n\n### 6.2 AOI Grid Algorithm—Notifying Only \"People Nearby\"\n\nThis is the most elegant solution to resolve event floods, inspired by a classic algorithm in game server development: **AOI (Area of Interest)**.\n\nThe core idea is incredibly simple: **If something happens in Area A, notify only the people in Area A and its immediately adjacent areas; people far away do not need to know.**\n\nSpecific implementation: Divide the entire virtual world into a Grid. Each grid cell maintains a list of \"residents currently here.\" When a broadcast event occurs, only query the current grid cell and its surrounding 8 grid cells (a 3x3 grid neighborhood), and deliver the event to the residents inside them.\n\n```text\n+---+---+---+\n| ↖ | ↑ | ↗ |\n+---+---+---+\n| ← | ★ | → |   ★ = Location of the event\n+---+---+---+\n| ↙ | ↓ | ↘ |\n+---+---+---+\n\nOnly notify residents in these 9 grid cells\n```\n\nThis small change directly reduces the complexity of \"notifying everyone\" from **O(N)** (where N is the total number of residents in the world) to nearly constant time **O(1)**—no matter how many residents there are in the world, the cost of a broadcast remains basically constant.\n\n---\n\n## Chapter 7: Integrating the Dynamic World with the Static Graph—The Closed Loop\n\nNow the execution logic of the entire virtual world is clear:\n1. Each resident is an independent Actor with a private state and mailbox.\n2. Residents communicate via events without directly interfering with one another.\n3. The Event Bus handles efficient dispatching, and the AOI algorithm controls the broadcast range.\n\nBut there is one more question: all states run in memory; how do we persist those complex **relationships**? For example, \"who is friends with whom\", \"who owns which piece of land\", \"who once appeared where\"—these relationship data are exactly what graph databases are best at handling.\n\nThis is where `GopherGraph`, introduced in the previous article, comes in handy.\n\nWe adopted a pattern called **Event Sourcing**: while the Event Bus dispatches events, it also asynchronously writes all key events into `GopherGraph`.\n\n```text\nReal-time Memory Layer (The engine in this article)\n            │\n            ▼ (Continuous asynchronous event stream logging)\nPersistent Graph Layer (GopherGraph)\n```\n\nIn this way, the memory layer is responsible for millisecond-level real-time interaction and state updates, while the graph layer handles the settled relationship data, providing complex historical query and data analysis capabilities. The two layers perform their respective duties without interfering with each other.\n\n---\n\n## Conclusion: What This Architecture Resolves and What It Sacrifices\n\nEvery architectural design comes with trade-offs. Let's make an honest summary:\n\n### ✅ What We Solved:\n* **Deadlock Issues**: The Actor model + Channels completely eliminates Mutexes, reducing the risk of deadlocks to a minimum.\n* **Performance Bottlenecks**: True parallel execution, fully utilizing multi-core CPUs.\n* **Code Coupling**: Event-driven design completely decouples each module. Adding a new resident behavior does not require modifying any existing code.\n* **Scalability**: Decoupling the Event Bus by region makes it theoretically possible to scale horizontally across multiple servers.\n\n### ⚠️ The Cost We Paid:\n* **Increased Debugging Complexity**: The call chain of asynchronous events is very long. When a bug occurs, it is sometimes difficult to trace which step went wrong. We specifically developed an event log tracing system for this.\n* **Increased Cognitive Load**: Developers must always think in an \"everything is asynchronous\" mindset. Shifting from synchronous calls requires a learning curve.\n* **Eventual Consistency**: Due to asynchronous communication, different entities may see slightly different states of the world at any given moment. The business logic must accept \"eventual consistency\" rather than \"strong consistency\".\n\n---\n\n**Go provides the natural soil for this architecture.** Thousands of Goroutines are a breeze for the Go Runtime, the `channel` and `select` syntax make concurrent communication write elegantly and naturally, and the `context` package makes lifecycle management extremely straightforward.\n\nI hope this article gives you a new perspective on concurrent system design. If you have any questions about the Actor model, event-driven design, or the specific implementation of `GopherGraph`, please feel free to leave a comment in the discussion section. See you next time!\n","AI",38,"https:\u002F\u002Fblog4-1316398321.cos.ap-nanjing.myqcloud.com\u002Fblog5\u002F20260625054209__公路-卡通世界.png",[15,16],"GO","Gemini",true,{"id":19,"title":20,"title_en":21},47,"用 Go 从零实现一个 AI Agent 后台系统","Implement an AI Agent backend system from scratch using Go",{"id":23,"title":24,"title_en":25},49,"Go + Gemini 多模型并发：让不同大脑同台竞技，以及差点打爆 API 的那段经历","Go + Gemini Multi-Model Concurrency: Putting Different “Brains” Head-to-Head—and the Time We Almost Crashed the API","2026-06-24T10:44:30.755601+08:00"]