不能直接调用 os.open 或 ioutil.readfile 做测试,因真实 i/o 会导致测试不可靠:残留文件致重复失败、路径分隔符跨平台不一致、ci 权限不足引发 panic、并发删错或漏删;错误信息难区分逻辑 bug 与环境问题;且 go 不支持 monkey patch 包级函数。

为什么不能直接调用 os.Open 或 ioutil.ReadFile 做测试
真实文件操作会让测试不可靠:同一测试反复运行可能因残留文件失败;os.TempDir() 在 Windows 返回带反斜杠的路径,Linux/macOS 是正斜杠,拼接路径时容易出错;CI 环境常无写权限,os.MkdirAll 直接 panic;哪怕加了 defer os.Remove,并发执行时仍可能删错文件或漏删。更麻烦的是,错误信息里出现 permission denied 或 no such file or directory 时,你根本分不清是逻辑 bug 还是环境问题。
必须用接口抽象,而不是 patch os.* 函数
Go 不允许对包级函数打桩(如 monkey patch os.Stat),强行 hack 会破坏类型安全,且在新版本中大概率失效。正确做法是定义最小接口,例如:
type FS interface {
Open(name string) (File, error)
Stat(name string) (os.FileInfo, error)
ReadFile(name string) ([]byte, error)
}
注意三点:
- 不要照搬
os包全部方法,只保留业务真正用到的 - 返回值类型尽量用具体类型(如
os.FileInfo),避免再抽象一层导致 mock 复杂化 - 函数参数和返回值保持与标准库一致,方便后续切换回真实实现
手写 MockFS 比引入 afero 更可控
小项目或单个文件读写场景下,一个结构体 + 字段函数就足够:
type MockFS struct {
ReadFileFunc func(string) ([]byte, error)
StatFunc func(string) (os.FileInfo, error)
}
func (m *MockFS) ReadFile(name string) ([]byte, error) {
if m.ReadFileFunc != nil {
return m.ReadFileFunc(name)
}
return nil, os.ErrNotExist
}
func (m *MockFS) Stat(name string) (os.FileInfo, error) {
if m.StatFunc != nil {
return m.StatFunc(name)
}
return nil, os.ErrNotExist
}
测试中可灵活控制行为:
- 模拟文件存在:
ReadFileFunc: func(_ string) ([]byte, error) { return []byte("key=val"), nil } - 模拟文件不存在:
StatFunc: func(_ string) (os.FileInfo, error) { return nil, os.ErrNotExist } - 验证是否被调用:在
ReadFileFunc里设标志位或计数器
别忽略 io/fs.FS 接口的兼容性陷阱
Go 1.16+ 引入了 io/fs.FS,它只定义 Open(name string) (fs.File, error),不包含 ReadFile。如果你的代码依赖 os.ReadFile,直接传 io/fs.FS 实现会编译失败——因为 os.ReadFile 内部仍调用的是 os.Open + 读取逻辑,它不接受 io/fs.FS。
所以实际选型要分清:
- 仅需读取嵌入文件(如
embed.FS):用io/fs.FS+fs.ReadFile - 需要完整模拟磁盘行为(含
Stat、WriteFile):自定义接口,或用afero.Fs(但得接受额外依赖) - 已有大量
os.XXX调用:优先封装成你自己的FS接口,而非强转适配标准接口
最易被忽略的一点:mock 结构体的方法接收者必须是指针类型(*MockFS),否则在依赖注入时传值会丢失状态,比如计数器不会递增、闭包捕获的变量无法更新。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











