测试中直接读写真实文件会破坏隔离性、引发并发冲突和权限问题;应使用os.MkdirTemp创建临时路径并defer os.RemoveAll清理;通过io.Reader/io.Writer接口解耦,用Strings.NewReader或io.Pipe模拟输入,避免内存暴涨与fd泄露。

用 ioutil.ReadFile 和 os.WriteFile 做简单测试会踩哪些坑
直接在测试里读写真实文件路径,看似快,实则破坏测试隔离性。比如并发跑测试时多个 goroutine 同时写 test.txt,结果不可控;CI 环境没写权限还会 panic。
真正安全的做法是:每次测试前生成唯一临时路径,用完立刻清理。别依赖固定路径或当前目录。
- 用
os.MkdirTemp("", "test-")创建临时目录,返回路径和可能的错误 - 测试末尾务必调用
os.RemoveAll(tempDir),建议用defer包裹 - 避免硬编码
"./data"或"/tmp/test"—— 这些在 docker 容器或 windows 下行为不一致
如何用 io.Reader / io.Writer 接口解耦文件操作
把具体文件操作封装进函数参数,而不是在函数内部直接调用 os.Open 或 os.Create,测试时就能传入 bytes.Buffer 或 strings.NewReader 替代真实文件句柄。
例如处理配置文件的函数,不要这样写:
立即学习“go语言免费学习笔记(深入)”;
func LoadConfig() (map[string]string, Error) { f, _ := os.Open("config.yaml") defer f.Close() // ... }
而应该改成:
func LoadConfig(r io.Reader) (map[string]string, error) { data, _ := io.ReadAll(r) return parseYAML(data) }
- 测试时传
strings.NewReader("key: value"),完全跳过磁盘 I/O - 生产调用时用
os.Open("config.yaml")作为r - 注意:
io.Reader不支持Seek,如果逻辑需要重读(如解析 jsON 多次),得先用io.ReadAll转成[]byte
用 os.File 模拟失败场景(权限拒绝、文件不存在)
真实错误很难稳定复现,但可以用 os.ErrPermission、os.ErrNotExist 等预定义变量手动构造错误,验证你的错误处理分支是否健壮。
比如你写了这样的逻辑:
if errors.Is(err, os.ErrNotExist) { return defaultConfig }
测试它不能只靠删掉文件再运行——CI 可能没权限删,或者删了别的测试会崩。更稳的方式是 mock 返回值:
- 把文件操作包装成可注入的函数类型,如
type FileReader func(name string) ([]byte, error) - 测试中传入闭包:
func(_ string) ([]byte, error) { return nil, os.ErrNotExist } - 避免用
os.Rename或os.Chmod改系统状态来触发错误——副作用大、难清理
测试大文件读写时为什么不能用 bytes.Buffer 全量加载
bytes.Buffer 本质是内存 slice,模拟几百 MB 文件会导致测试内存暴涨甚至 OOM,而且掩盖了流式处理的真实问题(比如未检查 io.EOF、缓冲区未 flush)。
正确做法是用 io.Pipe 搭建可控的流管道:
pr, pw := io.Pipe() go func() { defer pw.Close() io.Copy(pw, sourceReader) // sourceReader 可以是文件、网络响应等 }() // 把 pr 当作输入传给被测函数 process(pr)
-
io.Pipe是惰性执行,不会一次性把所有数据 load 到内存 - 可以控制写入速度(加
time.Sleep)、中途关闭(pw.Close()触发io.ErrClosedPipe) - 注意:别漏掉
pw.Close(),否则pr.Read会永远阻塞
真实文件测试里最容易被忽略的,是忘记验证 Close() 是否被调用 —— 尤其是 *os.File,不关会导致 fd 泄露,在长时间运行的服务里迟早出事。