应避免在单元测试中直接调用os.readfile/writefile,改用os.createtemp创建临时文件并defer清理,或用bytes.buffer/strings.newreader模拟i/o,错误处理需用errors.is断言,注意文件权限和跨平台兼容性。

直接在测试里调用 os.ReadFile 或 os.WriteFile 读写真实文件,等于放弃单元测试的隔离性、可重复性和速度——它不是“能跑就行”,而是埋下 CI 失败、并发冲突、残留文件和跨平台路径错误的定时炸弹。
用 os.CreateTemp 创建隔离临时文件再清理
硬编码 "./test.txt" 或 "/tmp/test" 是最常见也最危险的写法。Windows 路径分隔符不同、Docker 容器里 /tmp 可能不可写、多个 go test -race 并发运行时会互相覆盖。
-
os.CreateTemp("", "test-*.log")由系统分配唯一路径,自动避开冲突 - 必须用
defer os.Remove(tmpFile.Name())清理,否则多次运行后磁盘被占满 - 别忘了
defer tmpFile.Close(),否则文件句柄泄漏,Linux 下可能触发too many open files - 传给待测函数的是
tmpFile.Name()(字符串路径),不是*os.File——这样才符合生产调用方式
用 bytes.Buffer 或 strings.NewReader 替代磁盘 I/O
如果你的函数签名接收 io.Reader 或 io.ReadCloser,就完全没必要碰磁盘。内存模拟快、可控、断言直观,且天然支持边界数据(比如空内容、超长字符串、非法 UTF-8 字节)。
- 读文本:用
bytes.NewBufferString("key: value\nport: 8080") - 读 JSON:用
io.NopCloser(bytes.NewBufferString(`{"name":"test"}`))(满足io.ReadCloser接口) - 读二进制:用
bytes.NewBuffer([]byte{0xff, 0x00, 0xaa}) - 注意:
strings.NewReader不支持Seek,如果逻辑里调了r.Seek(0, io.SeekStart),得先转成[]byte再用bytes.NewReader
手动构造标准错误验证错误处理分支
靠“删掉文件再跑测试”来触发 os.ErrNotExist 是不可靠的:CI 权限受限、其他测试可能依赖该文件、清理不干净会导致后续测试失败。
- 直接返回预定义错误:
return nil, os.ErrNotExist或return nil, os.ErrPermission - 用
errors.Is(err, os.ErrNotExist)断言,而不是err == os.ErrNotExist——因为有些包装错误(如fmt.Errorf("read config: %w", os.ErrNotExist))需要errors.Is才能匹配 - 若函数内部调用了
os.Stat,就 mock 它的返回;不要 mockos.ReadFile本身——那属于侵入式 patch,破坏了 Go 的正交设计哲学
别用 ioutil,也别漏掉文件权限参数
ioutil.ReadFile 和 ioutil.WriteFile 在 Go 1.16+ 已彻底移入 os 包,继续 import "ioutil" 会编译失败。而 os.WriteFile 的第三个参数是 fs.FileMode,漏掉它默认是 0o600(仅用户可读写),但很多场景需要 0o644(组和其他人可读)。
- 写配置文件时用
os.WriteFile(path, data, 0o644),否则 Docker 容器里非 root 用户可能读不了 - 读取失败时,
os.IsNotExist(err)比errors.Is(err, os.ErrNotExist)更轻量,但语义一致,任选其一即可 - 别用
os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0)替代os.WriteFile——前者权限位被忽略,容易误设为0o000
真正难的不是写出能通过的测试,而是让每个 os 调用都有对应、可注入、可断言的抽象层;临时路径、内存 Reader、标准错误三者配合,才能把文件操作从“外部依赖”变成“可控输入”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











