必须显式调用os.open后立即defer f.close(),因gc不回收文件描述符;闭包传参需避免变量共享;临时文件须手动os.remove;长期模块应单例+读写锁管理文件句柄;并发需硬限流,如用带缓冲channel控制活跃goroutine数。

os.Open + defer Close 是必须的,不是可选的
哪怕只读一次小文件,也得显式调用 os.Open 后立刻配 defer f.Close()。Go 不会自动回收 fd,GC 只管内存,不管操作系统句柄。漏掉 Close() 是 too many open files 最常见原因。
- 闭包里传参要小心:写
go func(file string) { ... }(filename),别写go func() { ... }(filename),否则所有 goroutine 共享同一个filename变量,可能打开错误路径或重复打开同一文件 - 别信“临时文件等 GC”——
os.CreateTemp创建的文件必须手动os.Remove,fd 不会随文件删除自动释放,但文件删了再Close会报bad file descriptor - 日志轮转、配置热加载这类长期运行的模块,要用单例 +
sync.RWMutex管理*os.File,避免反复Open/Close
并发数必须硬限流,不能靠系统扛
开 1000 个 goroutine 去读 1000 个文件?大概率触发 too many open files,尤其在默认 ulimit -n=1024 的 Linux 上。真正起作用的是带缓冲的 channel 或信号量,不是 WaitGroup。
- 用
sem := make(chan struct{}, 10)控制最多 10 个活跃 goroutine,每个任务开始前sem ,结束后 <code> - 不要用
runtime.GOMAXPROCS控制 IO 并发——那是 CPU 密集型用的,IO 密集型靠的是系统 fd 和磁盘队列深度 - 如果文件都在同一块 SSD 上,盲目提高并发反而增加寻道延迟;实测 4–16 路并发往往吞吐最高,具体看
iostat -x 1的 %util 和 await
大文件别用 os.ReadFile,改用 io.Copy + bufio
os.ReadFile 会把整个文件读进内存,GB 级文件直接 OOM;而 io.Copy 流式转发 + bufio.Reader 分块读取,内存占用可控且系统调用更少。
- 下载/上传场景:直接
io.Copy(dst *os.File, src io.Reader),不经过内存中转,响应体多大都不怕 - 解析日志或 CSV:用
bufio.NewScanner(f)逐行读,比f.Read手动管理 buffer 更安全 - 需要 seek 或随机读的大文件(如数据库快照),用
file.Seek+ 固定大小buf := make([]byte, 64*1024)配合file.Read(buf),别用bufio(它内部维护偏移,seek 后状态不同步)
sync.Pool 不能复用 *os.File,但能复用 buffer
*os.File 包含不可复制的 fd 和内核状态,放进 sync.Pool 再拿出来大概率 panic 或数据错乱。但 []byte 缓冲区完全可以用池化降低 GC 压力。
- 声明全局池:
var bufferPool = sync.Pool{New: func() interface{} { return make([]byte, 1 - 每次读前
buf := bufferPool.Get().([]byte),用完bufferPool.Put(buf) - 注意:buf 归还前别保留其引用(比如塞进 map 或 channel),否则 Pool 无法回收,等于内存泄漏
真正卡住的点不在“怎么开”,而在“谁没关”和“开多少合适”。lsof -p $PID 和 pprof 查阻塞在 Close 或 Read 的 goroutine,比猜代码快得多。











