之前用这工具重构了早期的 ngroke,重构后的项目为 tikrok,用来反向代理,同样免费使用。现在总结一下这个工具的实现吧。
https://gitee.com/Talbot3/Mergdown
核心数据流程图
[命令行参数]
→ 参数解析
→ 目录遍历 → 路径过滤(类型/正则/排除)
→ 任务队列 → 并发处理器
→ 有序结果收集 → 文件流式写入
关键数据流分析
- 输入阶段
参数结构体{
TargetDir string // 目标目录路径
OutputPath string // 输出文件路径
MergeTypes []string // 文件类型过滤
RegexPattern string // 路径匹配正则
ExcludeDirs []string // 排除目录模式
}
- 目录遍历阶段
文件系统树 → WalkDir遍历 → 生成绝对路径列表
↓
路径转换 → 相对路径计算 → 过滤检查(三步验证):
1. 文件类型扩展名检查(MergeTypes)
2. 正则路径匹配(RegexPattern)
3. 排除目录匹配(ExcludeDirs)
↓
通过检查 → 生成有序任务队列(带序号索引)
- 并发处理阶段
任务队列 → 工作池(NumCPU数量) → 文件处理管道{
↓
打开文件 → 二进制检测 → 内容处理:
├─ 文本文件 → 流式读取(4KB块)
└─ 二进制 → 生成占位文本
↓
Markdown片段生成(带原始序号)
}
↓
结果通道(保序缓冲队列)
- 输出阶段
结果收集器 → 顺序重组 → 缓冲写入(64KB块) → 最终输出文件
↓
错误处理 → 快速失败(首个错误终止流程)
关键数据结构
- 任务结构体
type task struct {
index int32 // 全局自增序号(atomic控制)
path string // 文件绝对路径
}
- 结果结构体
type result struct {
index int32 // 对应任务序号
content chan []byte // 流式内容管道(避免大文件内存问题)
err error // 错误信息
}
流量特征分析
- 内存控制机制
- 滑动窗口写入:每个文件的处理结果通过channel分块传输
- 双缓冲策略:收集器使用:
- 内存缓存:64KB缓冲写
- 磁盘缓冲:os.File的自动页缓存
- 并发控制参数
最大并行文件数 = min(runtime.NumCPU(), maxOpenFiles)
默认值:100(防止文件描述符耗尽)
异常流处理
- 错误传播路径
文件读取错误 → 结果通道 → 收集器 → 主协程终止
参数校验错误 → 直接退出
系统调用错误 → 日志记录+跳过文件
- 恢复机制
defer func() {
if r := recover(); r != nil {
log.Printf("Critical error: %v", r)
os.Exit(1)
}
}()
性能优化点
- 零拷贝优化
文件读取 → 块复制 → 直接写入channel(避免多次内存分配)
- 路径缓存重用
相对路径计算 → 每个文件仅计算1次 → 用于:
- 正则匹配
- 排除检查
- Markdown标题生成
- 智能预读策略
// 二进制检测时预读512字节
header := make([]byte, headerReadLen)
n, _ := file.Read(header)
file.Seek(0, io.SeekStart) // 重置读指针
典型数据流示例
输入场景
./mergetool -mergtype go -exclude "testdata" -regex ".*service.*" ~/project
数据流轨迹
- 遍历
~/project
目录下所有文件 - 过滤非.go扩展名文件
- 排除路径包含
testdata
的目录 - 匹配路径包含
service
关键字 - 生成合并文档:
project.md
内存变化曲线
初始内存:2MB(基础运行时)
处理峰值:约32MB(100个文件并发处理时)
稳定状态:约8MB(流式写入保持稳定)