goroutine泄漏比性能差更危险,会耗尽内存、卡死调度器、阻碍pprof定位;应限流、设超时、及时关闭io资源。

goroutine 泄漏比性能差更危险
开太多 goroutine 不会直接拖慢程序,但会耗尽内存、卡死调度器、让 pprof 难以定位问题。尤其在处理成百上千个文件时,常见写法是为每个文件起一个 go processFile(filename),结果没加控制——文件一多,几万 goroutine 堆着不动,runtime.GOMAXPROCS 都救不了。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用带缓冲的
chan struct{}或semaphore(如golang.org/x/sync/semaphore)限流,比如最多并发 10 个 IO 操作 - 避免在 goroutine 内部无条件等待:比如
io.Copy读取网络文件时没设超时,一个卡住就挂住整个 goroutine - 用
runtime.NumGoroutine()在日志里定期打点,上线前压测时观察是否持续上涨
os.Open + io.Copy 比 ioutil.ReadFile 快得多
ioutil.ReadFile(或 os.ReadFile)看似方便,但它把整个文件读进内存再返回 []byte。对几百 MB 的日志文件,一次调用就分配巨量堆内存,GC 压力陡增,还可能触发 OOM。而真实 IO 密集型任务(如转码、压缩、行过滤)往往不需要全载入。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 优先用
os.Open获取*os.File,再配bufio.NewReader或直接io.Copy流式处理 - 写目标文件时也用
os.Create+io.Copy,避免中间拼接字符串或切片 - 注意
io.Copy默认缓冲区是 32KB,对 SSD 可能偏小;若确认是顺序大文件,可传自定义io.CopyBuffer提升吞吐
filepath.WalkDir 比 filepath.Walk 更可控
filepath.Walk 是递归回调模型,一旦在回调里起 goroutine,父子目录遍历顺序和并发节奏完全不可控,容易造成文件句柄泄漏或磁盘寻道混乱。filepath.WalkDir(Go 1.16+)返回迭代器,你可以自己决定什么时候读、什么时候发、什么时候等。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
filepath.WalkDir遍历路径,把文件信息(fs.DirEntry)发到一个 channel,由固定数量 worker 消费 - 跳过符号链接、权限不足目录时,
WalkDir允许在DirEntry上调Info()后主动return nil,比Walk的 error 处理更干净 - 别在
WalkDir回调里做阻塞操作(如time.Sleep或同步 HTTP 请求),它运行在单个 goroutine 中,会拖慢整个遍历
sync.WaitGroup + context.WithTimeout 是必须组合
并发处理文件最怕“一半成功一半卡死”,比如某个远程文件 URL 响应极慢,整个批量任务就僵在那里。只用 WaitGroup 等所有 goroutine 结束,等于放弃超时控制;只用 context 不等完成,又可能提前退出丢数据。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 启动 worker 前创建带 timeout 的
ctx := context.WithTimeout(context.Background(), 30*time.Second) - 每个 goroutine 入口检查
ctx.Err() != nil,立即 return -
WaitGroup的Done()放在 defer 里,确保无论正常结束还是 ctx 超时都能通知主 goroutine - 主 goroutine 等待时用
select等WaitGroup完成或ctx.Done(),别只写wg.Wait()
真正卡住性能的,往往不是 goroutine 数量,而是每个 goroutine 里那个没关的 *os.File、那个没设 deadline 的 http.Client、或者那个忘了用 defer 关闭的 gzip.Reader。IO 并发不是开得越多越好,是关得越及时越稳。










