> ## Documentation Index
> Fetch the complete documentation index at: https://docs.akria.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Day 24 | 并发编程 (Goroutine)

> Go 语言的杀手锏：Goroutine 轻量级线程与 Channel 通信

<Info>
  **今日目标**：Python 的多线程受到 GIL 的限制，属于“伪并发”。而 **Go 的并发是真并行**。今天我们将学习如何启动成千上万个轻量级线程 (Goroutine)，并使用 Channel 在它们之间优雅地传递数据，实现 **“不要通过共享内存来通信，而要通过通信来共享内存”** 的 Go 核心哲学。
</Info>

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="book" size={26} color="#059669" /> 学习内容 (30 mins)</span>

在开始写代码前，先搞懂这些核心概念，否则后面的代码你会看得云里雾里。

<AccordionGroup>
  <Accordion title="Goroutine (协程) (10 mins)" icon="microchip">
    **为什么 Go 能轻松支持百万级并发？**

    * **OS 线程**: JVM 或 Python 调用的通常是操作系统线程，一个就要消耗约 2MB 内存，启动和切换都很慢。
    * **Goroutine**: 这是由 Go 运行时管理的“用户态线程”。启动一个只需约 **2KB** 内存。
    * **使用方法**: 在任何函数前输入 `go` 即可（例如 `go task()`）。
    * **生命周期**: 主线程 `main` 像是一个指挥官，如果指挥官下班了（`main` 执行结束），他在外面派出的所有斥候（Goroutines）都会被强制瞬间销毁。此特性需要特别注意。
  </Accordion>

  <Accordion title="Channel (通道) (10 mins)" icon="conveyor-belt">
    **并发里的安全传送带**

    * **痛点**: 最怕多个线程同时抢着改同一个变量（竞态条件），导致数据乱套。
    * **Go 的解决方案**: 既然抢数据会打架，那我们就把数据通过 **Channel** 传过去，谁接到谁处理。
    * **语法**:
      * 创建: `ch := make(chan int)`
      * 发送: `ch <- 1` (向通道塞数据)
      * 接收: `x := <-ch` (从通道拿数据)
    * **阻塞特性**: 无缓冲的 channel，发送方会阻塞直到有人接收，接收方也会阻塞直到有人发送。这种阻塞是天生的同步机制。
  </Accordion>

  <Accordion title="WaitGroup (同步) (10 mins)" icon="users">
    **如何优雅地等待？**

    `sync.WaitGroup` 是一个计数器，用来等待一组 Goroutine 完成。

    * `Add(n)`: “报告长官，派出了 n 个人！”
    * `Done()`: “报告长官，我干完活了！”（计数器 -1）
    * `Wait()`: 阻塞主线程，直到计数器归零。
  </Accordion>
</AccordionGroup>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="code" size={26} color="#059669" /> 代码任务 (90 mins)</span>

<Steps>
  <Step title="任务 A: 感受并发的“随机性”">
    通过这个例子，你会理解为什么并发程序需要协调。

    <CodeGroup>
      ```go 24_race.go theme={null}
      package main

      import (
          "fmt"
          "time"
      )

      func task(name string) {
          for i := 1; i <= 3; i++ {
              // 模拟干活耗时 100ms
              time.Sleep(100 * time.Millisecond)
              fmt.Printf("[%s] is processing step %d\n", name, i)
          }
      }

      func main() {
          // 瞬间派出两名员工，它们会同时开始跑
          go task("Agent-001")
          go task("Agent-002")

          // ⚠️ 极其重要: 主线程必须等待
          // 如果没有下面这行 Sleep，main 函数会立即退出
          // 导致上面的两个协程还没来得及打印就被系统杀死了
          time.Sleep(500 * time.Millisecond) 
          fmt.Println("Main thread quit.")
      }
      ```
    </CodeGroup>

    **代码解释**：

    * **`go task("Agent-001")`**：启动一个 Goroutine，异步执行
    * **`time.Sleep(500 * time.Millisecond)`**：主线程等待，防止提前退出
    * **并发执行**：两个 Goroutine 同时运行，输出顺序不确定

    **Go 并发的设计哲学**：

    * **"不要通过共享内存来通信，而要通过通信来共享内存"**：这是 Go 的核心哲学
    * **轻量级**：Goroutine 比线程轻量得多，可以轻松创建成千上万个
    * **自动调度**：Go 运行时自动在多个 OS 线程上调度 Goroutine

    **验证步骤**：

    1. 运行程序，观察输出顺序。你会发现 Agent-001 和 Agent-002 的输出是交替的，而且每次运行的交替顺序可能都不一样。
    2. 尝试注释掉 `time.Sleep` 那一行，再运行。你会发现很可能什么都打印不出来，程序就结束了。
    3. 多次运行，观察输出顺序的变化，理解并发的非确定性

    **常见错误**：

    <Warning>
      * ❌ 忘记等待：主线程退出导致子协程被杀。
      * ❌ 竞态条件：如果两个 `task` 同时修改一个全局变量且没加锁，数据会出错（虽然本例全是打印，没有共享变量）。
    </Warning>
  </Step>

  <Step title="任务 B: 生产者-消费者模式 (Channel)">
    模拟一个“日志收集器”：抓取器负责生产日志，存储器负责消费并保存。这是 Channel 最经典的应用场景。

    <CodeGroup>
      ```go 24_channel.go theme={null}
      package main

      import (
          "fmt"
          "time"
      )

      // LogProducer 模拟日志抓取
      // chan<- 声明该 channel 只能用于发送内容 (Send Only)
      func LogProducer(logQueue chan<- string) {
          logs := []string{"User login", "API 404 error", "DB query timeout", "Backup success"}
          
          for _, msg := range logs {
              fmt.Println("🚀 [Scraper] Captured log:", msg)
              logQueue <- msg // 将日志塞入传送带 (如果没人接，这里会阻塞)
              time.Sleep(200 * time.Millisecond)
          }
          // 重要：发完了必须关闭，否则消费者会永远等待（导致死锁）
          close(logQueue)
      }

      // LogConsumer 模拟日志持久化
      // <-chan 声明该 channel 只能用于接收内容 (Receive Only)
      func LogConsumer(logQueue <-chan string, done chan<- bool) {
          // 使用 range 优雅地从 Channel 读取
          // 它会一直循环，直到 Channel 被 Close() 且数据读完
          for msg := range logQueue {
              fmt.Printf("💾 [Storage] Saving to DB: %s...\n", msg)
              time.Sleep(300 * time.Millisecond) // 模拟存入数据库比较慢
          }
          fmt.Println("✅ [Storage] All logs processed.")
          
          // 任务全部搞定，给主线程发个信号
          done <- true
      }

      func main() {
          // 1. 创建无缓冲通道（传送带）
          logQueue := make(chan string)
          // 2. 用于同步的完成状态通道
          done := make(chan bool)

          // 启动并发流程
          go LogProducer(logQueue)
          go LogConsumer(logQueue, done)

          // 3. 阻塞主线程，等待 done 信号
          fmt.Println("System running...")
          <-done // 如果没有这行，主线程会直接退出
          fmt.Println("System stopped.")
      }
      ```
    </CodeGroup>

    **代码解释**：

    * **`chan<- string`**：只发送通道（Send Only）
    * **`<-chan string`**：只接收通道（Receive Only）
    * **`close(logQueue)`**：关闭通道，通知接收方没有更多数据
    * **`for msg := range logQueue`**：优雅地从通道读取，直到通道关闭

    **Channel 的设计哲学**：

    * **通信即同步**：Channel 既是数据传递的通道，也是同步的机制
    * **阻塞特性**：无缓冲通道天然提供同步，发送方和接收方必须同时准备好
    * **关闭信号**：关闭通道是一种信号，表示没有更多数据

    **验证步骤**：

    1. 运行程序：`go run 24_channel.go`
    2. 观察生产者和消费者的协作过程
    3. 尝试注释掉 `close(logQueue)`，观察程序是否会死锁
  </Step>

  <Step title="任务 C: WaitGroup 并发大杀器">
    模拟同时对多个 IP 进行 Ping 操作。这是以后写并发脚本最常用的模板。

    <CodeGroup>
      ```go 24_waitgroup.go theme={null}
      package main

      import (
          "fmt"
          "sync"
          "time"
      )

      // 注意：wg 必须传指针 *sync.WaitGroup，否则是拷贝，Done() 不会生效！
      func checkHost(ip string, wg *sync.WaitGroup) {
          // 函数退出前，一定要通知计数器 -1
          // 使用 defer 是为了防止中间代码崩了导致没减计数，导致主线程死锁
          defer wg.Done()
          
          fmt.Printf("🔍 Checking host: %s\n", ip)
          time.Sleep(1 * time.Second) // 模拟网络延迟
          fmt.Printf("✅ %s is ONLINE\n", ip)
      }

      func main() {
          var wg sync.WaitGroup
          hosts := []string{"10.0.0.1", "10.0.0.2", "192.168.1.100", "8.8.8.8"}

          for _, ip := range hosts {
              wg.Add(1) // 派人，计数器 +1
              
              // 启动协程
              // 注意：这里传的是 &wg (指针)
              go checkHost(ip, &wg)
          }

          fmt.Println("⏳ Monitoring started. Waiting for all results...")
          
          // 这里会卡住，直到计数器归零
          wg.Wait()
          
          fmt.Println("🚀 All hosts checked. Report generated.")
      }
      ```
    </CodeGroup>

    **代码解释**：

    * **`*sync.WaitGroup`**：必须传指针，否则是副本，`Done()` 不会生效
    * **`defer wg.Done()`**：使用 defer 确保即使函数出错也会调用 Done
    * **`wg.Add(1)`**：在启动 Goroutine 前增加计数
    * **`wg.Wait()`**：阻塞直到所有 Goroutine 完成

    **WaitGroup 的设计哲学**：

    * **计数器模式**：通过计数跟踪 Goroutine 的完成状态
    * **必须传指针**：WaitGroup 是值类型，必须传指针才能共享状态
    * **defer 保护**：使用 defer 确保 Done 一定会被调用

    **验证步骤**：

    1. 运行程序：`go run 24_waitgroup.go`
    2. 观察所有主机检查的并发执行
    3. 尝试修改 `wg.Add(1)` 的位置，观察行为变化

    **常见错误**：

    <Warning>
      * ❌ 传值而不是传指针：如果在 `checkHost` 里写 `wg sync.WaitGroup`，那么那是 `wg` 的一个副本。你在副本上调用 `Done()`，主线程里的 `wg` 根本不知道，最后 `Wait()` 会永久卡死。
      * ❌ 忘记 `Add()`：如果你没有 Add 就 Wait，或者 Add 数量少于任务数，逻辑会错乱。一般建议在 `go` 关键字之前立即调用 `Add(1)`。
    </Warning>
  </Step>
</Steps>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="lightbulb" size={26} color="#059669" /> 拓展任务 (30 mins)</span>

<CardGroup cols={2}>
  <Card title="带缓冲的 Channel 挑战" icon="terminal">
    **任务**：将 `make(chan string)` 改为 `make(chan string, 10)`。

    **实验**：在 `LogProducer` 里不加 Sleep。观察运行行为。

    * **现象**: 生产者会瞬间把 4 条日志塞进去然后退出，而不是像无缓冲那样必须等消费者拿走一条才能塞下一条。
    * **思考**: 这种模式适合应对什么样的流量高峰？
  </Card>

  <Card title="Timeout 挑战 (Select 语句)" icon="clock">
    **任务**：查阅 `select` 关键字。

    **挑战**：在主线程接收 `<-done` 时，如果过了一秒还没干完，就打印“Timeout! Program terminated.”。这是防止程序hang死的常见手段。
  </Card>
</CardGroup>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="file" size={26} color="#059669" /> 今日产出物</span>

* `~/projects/learn-go/24_race.go` - 并发竞争演示
* `~/projects/learn-go/24_channel.go` - 经典解耦流水线
* `~/projects/learn-go/24_waitgroup.go` - 批量处理同步模板

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="github" size={26} color="#059669" /> 参考代码</span>

<CardGroup cols={2}>
  <Card title="查看参考代码" icon="code" href="https://github.com/akriamail/insightful-ops/tree/main/docs/mintlify/scripts/training/30daystudy/week-4/day-24">
    在 GitHub 查看完整的示例代码

    <br />

    <small>包含 Channel 和 WaitGroup 示例</small>
  </Card>

  <Card title="Go Playground" icon="play" href="https://go.dev/play/">
    使用在线编辑器运行 Go 代码

    <br />

    <small>验证并发逻辑</small>
  </Card>
</CardGroup>

***

## 实际应用场景

<CardGroup cols={2}>
  <Card title="为什么 Go 开发的 Agent 强？" icon="server">
    * **极低抖动**: 即使监控成千上万个文件，Goroutine 的上下文切换开销也极小，不会导致 CPU 突然飙升。
    * **原生的超时控制**: 在处理网络请求时，使用 `context` 结合 `select` 可以完美控制每个连接的超时，防止把系统卡死。
  </Card>

  <Card title="并发 vs 并行" icon="shuffle">
    * **并发 (Concurrency)**: 多个任务在**一个时间段**内运行（可能是单核上的快速切换）。
    * **并行 (Parallelism)**: 多个任务在**同一个时刻**同时运行（需要多核 CPU 支持）。
    * **Go 的优势**: 它自动帮你在这两者之间调度，你只需要关心 `go` 关键字。
  </Card>
</CardGroup>

<Tip>
  **与 Day 25 的关联**：今天掌握了“内功”原理，明天我们将把这些并发技巧应用到真正的运维场景中，开发一个**高并发端口扫描器**，实测它比 Python 版本快多少倍。
</Tip>

***

<CardGroup cols={2}>
  <Card title="回到目录" href="/training/30daystudy/week-4">
    查看完整进度
  </Card>

  <Card title="下一天: 工具开发 I" href="/training/30daystudy/day-25">
    Day 25 | Go 实战工具开发 (一)
  </Card>
</CardGroup>
