Go 中不能无限制启动 goroutine 处理文件 IO,因 os.File 是阻塞式 fd,goroutine 会在系统调用中挂起,导致 too many open files、OOM 或卡死;需用 channel 限流(如 make(chan struct{}, N))控制并发数,HDD 建议 ≤4,SSD 通常 4–8 最优。

Go 没有多线程概念,所谓“多线程并行处理 IO”实际是用 goroutine + 限流 + 正确的资源管理来模拟,硬套线程模型反而导致 too many open files、内存暴涨或 goroutine 泄漏。
为什么不能直接起一堆 goroutine 处理文件 IO
os.File 是阻塞式 fd,每个 os.Open + Read 都会挂起一个 goroutine,但 runtime 不会用 epoll 等机制唤醒它——内核在等磁盘响应时,goroutine 就卡在系统调用里。起 1000 个 goroutine 读 1000 个文件,不是快,是把文件描述符和内存全耗光。
- 常见错误现象:
too many open files、OOM、程序卡死不动 - 根本原因:没做并发控制,也没复用 buffer 或 file handle
- HDD 场景下,并发数 >4 反而因磁头频繁寻道变慢;SSD 也通常 4–8 并发吞吐最优
用 make(chan struct{}, N) 做轻量信号量限流
这是 Go 里最常用、最轻量的并发控制器,不传数据,只计数,比 sync.Mutex 更符合通信模型。
- 初始化:
sem := make(chan struct{}, 10)表示最多 10 个任务同时执行 - 每个任务开始前阻塞获取:
sem - 任务结束(无论成功失败)必须归还:
defer func() { - 千万别在 goroutine 里
close(sem)——信号量要复用,关了就废了 - 忘了
defer归还?后续所有任务会在卡死,整个流程停摆
结果收集必须带索引,别信执行顺序
goroutine 完成时间不确定,往共享 slice 写结果会竞态;闭包捕获循环变量 i 更是经典坑——所有任务都写进 results[3](如果循环到 4)。
立即学习“go语言免费学习笔记(深入)”;
- 定义结果结构:
type TaskResult struct { Index int; Data interface{}; Err error } - 开带缓冲 channel:
results := make(chan TaskResult, len(tasks)) - 启动时显式传参:
go func(idx int) { ... }(i),而不是依赖外层i - 主 goroutine 循环接收
len(tasks)次,按Index填入结果切片,自然保序
别碰“IO 多路复用优化文件读写”这种伪命题
Linux 的 epoll、FreeBSD 的 kqueue 不支持普通文件 fd,调 epoll_ctl 注册会直接返回 EINVAL;Go runtime 也没把 os.File I/O 接入 netpoll 调度路径。
- 所有
os.Read/os.Write最终走阻塞系统调用,goroutine 被挂起,不是事件驱动 -
bufio.Reader、io.Copy只是加了缓冲或批处理,底层仍是阻塞 read/write - 真有效的优化是:流式读写 + 并发节制 +
sync.Pool复用 buffer,比如bufio.NewReaderSize(f, 1 - 混合 IO 场景(如边读文件边发 HTTP)可用 channel 统一调度,但这只是逻辑流水线,不是内核级多路复用
真正容易被忽略的是:文件 IO 的瓶颈从来不在 Go 代码本身,而在磁盘寻道、连接池限制、下游限流或 ulimit 设置。并发数调再高,卡在 openat 系统调用上,就只是堆等待队列而已。


















