> ## 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 26 | Go 实战工具开发 (二)

> 实战：编写一个日志并发分析工具，挑战 GB 级数据流式处理

<Info>
  **今日目标**：运维最常见的场景就是“查日志”。当日志文件只有 10MB 时，Python 的 Pandas 很方便；但当日志达到 10GB 甚至 1TB 时，Go 的 **流式处理 (Streaming)** 和 **多核并行能力** 就能教做人了。今天我们将编写一个能快速从海量 access.log 中提取访问前十 IP 的工具。
</Info>

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

在开始写代码前，理解为什么传统的“一次性读取”方法在处理大文件时由于内存限制而不可行。

<AccordionGroup>
  <Accordion title="逐行扫描的艺术：bufio.Scanner (15 mins)" icon="filter">
    **内存友好的读取方式**

    * **错误做法**: `os.ReadFile()` 会尝试将整个文件加载到内存。如果文件只有 100MB 没事，但如果是 10GB 的日志，你的程序会瞬间 OOM崩溃。
    * **正确做法**: 使用 `bufio.NewScanner(file)`。它像个漏斗，每次只从硬盘里取一行数据到内存，处理完后再取下一行。无论文件多大，内存占用始终保持在几 KB。
  </Accordion>

  <Accordion title="并发架构：Pipeline 架构 (15 mins)" icon="arrows-split-up-and-left">
    **如何榨干 CPU？**

    * **IO 瓶颈**: 读硬盘是慢的，通常用一个协程专门负责读。
    * **CPU 瓶颈**: 字符串切割、正则匹配是消耗 CPU 的。如果单核跑，硬盘读得快但 CPU 算不过来。
    * **解决方案**:
      1. **Reader (1个)**: 负责读硬盘，把行塞进 `lines` 通道。
      2. **Worker (N个)**: 启动 8-16 个协程从通道抢行，并行进行字符串处理。
      3. **Map 安全**: 多个 Worker 同时写一个 Map 会 Panic，需要加锁或者用汇总 Channel。
  </Accordion>
</AccordionGroup>

***

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

<Steps>
  <Step title="准备模拟大数据">
    在动手写 Go 之前，先准备好靶场数据。我们需要造一个大文件来测试性能。

    **验证步骤**:
    在终端执行以下命令，生成一个约 50MB 的日志文件 `access.log`。

    ```bash theme={null}
    # 使用 Shell 快速生成 50 万行模拟 Nginx 日志 (约 50MB)
    # 格式: IP - - [Date] "Request" Status Size
    for i in {1..500000}; do 
      echo "192.168.1.$((RANDOM % 255)) - - [31/Jan/2024:22:00:00] \"GET /index.html HTTP/1.1\" 200 1234" >> access.log
    done
    ```
  </Step>

  <Step title="代码实战：并发分析器">
    我们将编写 `26_log_analyzer.go`，展示如何安全地在多个协程间共享计数结果。

    <CodeGroup>
      ```go 26_log_analyzer.go theme={null}
      package main

      import (
          "bufio"
          "fmt"
          "os"
          "strings"
          "sync"
          "time"
      )

      // 全局统计结果
      var (
          ipCounter = make(map[string]int)
          mu        sync.Mutex // 互斥锁，防止多个并发写 Map 导致 Panic
      )

      // Worker 函数：负责处理具体的字符串逻辑
      func analyzer(lines <-chan string, wg *sync.WaitGroup) {
          defer wg.Done()
          
          // 不断从通道取日志行
          for line := range lines {
              // 1. 极简提取方案 (strings.Split 比正则快 10 倍以上)
              // 假设日志格式为 "IP - - ...", 空格分割后的第一个元素就是 IP
              parts := strings.Split(line, " ")
              if len(parts) > 0 {
                  ip := parts[0]
                  
                  // 2. 加锁更新全局 Map
                  // 临界区要尽可能小，处理完数据再加锁
                  mu.Lock()
                  ipCounter[ip]++
                  mu.Unlock()
              }
          }
      }

      func main() {
          start := time.Now()
          
          // 打开文件
          file, err := os.Open("access.log")
          if err != nil {
              fmt.Println("Error:", err)
              return
          }
          defer file.Close()

          // 3. 通道与同步控制
          // 缓冲区设大一点 (5000)，防止读文件太快把通道塞满阻塞了 Reader
          lines := make(chan string, 5000) 
          var wg sync.WaitGroup

          // 启动 4 个分析 Worker (通常设置为 CPU 核心数)
          for i := 0; i < 4; i++ {
              wg.Add(1)
              go analyzer(lines, &wg)
          }

          // 4. 主线程负责读文件 (Producer)
          // 读硬盘通常是单线程的瓶颈，所以用主线程读即可
          scanner := bufio.NewScanner(file)
          fmt.Println("🚀 Analysis started...")
          
          count := 0
          for scanner.Scan() {
              lines <- scanner.Text() // 把读到的行发给 Worker
              count++
          }
          close(lines) // 读完关闭通道，通知 Worker 收工

          // 等待所有 Worker 处理完剩余数据
          wg.Wait()
          
          fmt.Printf("✅ Processed %d lines in %v.\n", count, time.Since(start))
          fmt.Printf("Unique IPs found: %d\n", len(ipCounter))
          
          // (可选) 这里可以加一个排序逻辑打印 Top 10，为简化代码略去
      }
      ```
    </CodeGroup>

    **代码解释**：

    * **`bufio.NewScanner(file)`**：逐行扫描，内存占用小
    * **`make(chan string, 5000)`**：带缓冲的通道，防止阻塞
    * **`sync.Mutex`**：互斥锁，保护共享的 Map
    * **`mu.Lock()` / `mu.Unlock()`**：临界区保护，防止并发写入

    **流式处理的设计哲学**：

    * **内存友好**：逐行处理，不一次性加载整个文件
    * **并发处理**：多个 Worker 并行处理，充分利用多核 CPU
    * **锁粒度最小化**：只在更新 Map 时加锁，减少锁竞争

    **验证步骤**：

    1. 运行程序：`go run 26_log_analyzer.go`
    2. 你应该能在 1秒内 处理完这 50MB 的数据
    3. 尝试把生成的日志文件大小增加到 1GB (改下生成脚本的循环次数)，再测一次，观察内存占用（应该不会超过 20MB）
    4. 观察多个 Worker 的并发处理，理解 Pipeline 架构

    **常见错误**：

    <Warning>
      * ❌ **Map 并发读写**：如果不加 `mu.Lock()`，程序会 100% 崩溃并在控制台报 `concurrent map writes` 错误。Go 的 Map 默认不是线程安全的。
      * ❌ **Channel 死锁**：如果忘记 `close(lines)`，Worker 会永远等待下去，程序永远不会结束。
    </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="消除锁竞争 (进阶)" icon="bolt-lightning">
    **挑战**：在上面的代码中，所有 Worker 都在抢一把 `mu` 锁，效率其实打了折扣。

    **思路**：

    1. 每个 Worker 自己维护一个 `localMap`。
    2. 循环结束后，把 `localMap` 发送到一个新的 Channel。
    3. 主线程最后只合并这 4 个 `localMap`。这样就完全没有锁竞争了！
  </Card>

  <Card title="JSON 日志解析" icon="file-code">
    **挑战**：如果日志不是 Nginx 格式，而是 JSON 格式 `{"ip": "...", "status": 200}`？

    **提示**：使用 `encoding/json` 库。注意 JSON 解析是比较慢的，这时候多核并发的优势会更加明显。
  </Card>
</CardGroup>

***

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

* `access.log` - 模拟的大型日志文件
* `~/projects/learn-go/26_log_analyzer.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-26">
    在 GitHub 查看完整的示例代码

    <br />

    <small>包含无锁化 Map 的高级实现</small>
  </Card>

  <Card title="Go Bufio 文档" icon="book" href="https://pkg.go.dev/bufio">
    阅读官方 Scanner 文档

    <br />

    <small>了解 SplitFunc 自定义分割逻辑</small>
  </Card>
</CardGroup>

***

## 实际应用场景

<CardGroup cols={2}>
  <Card title="流式处理的威力" icon="water">
    * **内存占用**: Python 读取 1GB 文件可能需要 2-3GB 内存（加载整个对象）。Go 的流式处理稳定在 20MB。
    * **实时性**: 你可以把 `os.Open` 换成对 `os.Stdin` 的读取，然后通过管道 `tail -f access.log | ./analyzer` 实时分析日志流。
  </Card>

  <Card title="ELK 的替代品？" icon="server">
    * 在很多小微企业，搭建一套 ELK (Elasticsearch) 太重了。
    * 一个编译好的 Go 工具，配合 Cron 定时任务，足以应付每天几百 GB 的日志统计需求，且零成本维护。
  </Card>
</CardGroup>

<Tip>
  **与 Day 27 的关联**：经过三天的 Go 语言实战，你已经感受到了它的速度。明天我们将进行一次全面的**技术选型大复盘**：到底什么时候该用 Python？什么时候该用 Go？
</Tip>

***

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

  <Card title="下一天: 复盘与选型" href="/training/30daystudy/day-27">
    Day 27 | 复盘与技术选型
  </Card>
</CardGroup>
