ci中硬件异常模拟需在软件接口层注入错误:识别被测代码依赖的最高层可替换接口(如fs.fs、io.reader、http.roundtripper、driver.conn),实现返回标准错误(如*fs.patherror、url.error)的fake,禁用缓存并显式清理资源。

CI 中无法真正触发硬件异常(如磁盘坏道、内存 ECC 错误、PCIe 链路中断),所有“模拟”必须落在软件可控制的接口层——核心是识别被测代码在哪一层暴露了对硬件的依赖,并在那一层注入可控错误。
识别真实错误暴露点:别在 syscall 层硬塞 ENODEV
Go 程序从不直接和硬件对话。所谓“硬件异常”,最终体现为某个标准库函数返回特定错误:os.Open 返回 syscall.ENODEV,io.Copy 返回 syscall.EIO,net.Conn.Read 返回 syscall.ECONNRESET。但这些错误码由内核返回,用户态无法伪造 syscall 调用结果。
- 强行用
mount --bind或dmsetup在 CI 容器里制造设备消失?权限受限、不可靠、清理困难,且多数 CI 运行在无特权容器中 - 正确做法是:找到被测代码调用的最上层可替换接口,比如
fs.FS、io.Reader、database/sql/driver.Conn,然后实现一个返回对应错误的 fake - 例如测试“磁盘设备离线”逻辑,不要 mock
syscall.Open,而是让业务代码接收fs.FS参数,传入一个在Open()方法里返回&fs.PathError{Op: "open", Path: "/dev/sdb", Err: syscall.ENODEV}的实现
用 fs.FS 和 io 包接口统一模拟存储类异常
Go 1.16+ 的 io/fs 是抽象文件系统行为的事实标准。只要把 os.ReadFile、os.WriteFile 替换为接受 fs.FS 的版本,就能在测试中完全隔离真实磁盘。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 定义一个
faultyFS结构体,嵌入fs.ReadFileFS(如os.DirFS),重写Open()方法:对特定路径返回syscall.ENOSPC,对另一些路径返回syscall.EACCES - 注意:错误必须是 *fs.PathError 类型,且
Err字段设为原始 syscall 错误值(不是字符串),否则errors.Is(err, syscall.ENOSPC)会失败 - 对流式 I/O(如日志写入),用自定义
io.Writer实现,在第 N 次Write()时返回&fs.PathError{Op: "write", Path: "/var/log/app.log", Err: syscall.EIO} - 避免在
Write()中 panic 或返回errors.New("disk write failed")—— 调用方通常只检查errors.Is(err, syscall.EIO)做重试或降级
网络与驱动类异常:用 http.RoundTripper 和 database/sql/driver 模拟
网卡故障、USB 设备拔出等场景,在 Go 中表现为 http.Client 或数据库驱动的连接/读写错误。这些接口明确允许返回任意 error,是理想的注入点。
- 实现一个
faultyTransport,包装真实http.Transport,在RoundTrip()中按概率或条件返回&url.Error{Op: "dial", Err: syscall.ENETUNREACH} - 数据库层面,不要 mock
*sql.DB,而是实现database/sql/driver.Conn接口:在Prepare()或Exec()方法中直接返回driver.ErrBadConn或带syscall.EIO的自定义错误 - 关键细节:HTTP 错误需满足
url.Error结构,DB 错误需满足driver.ErrBadConn或实现Is(error) bool方法,否则上层框架(如sqlx)可能忽略重试逻辑
CI 测试中必须禁用缓存并显式清理资源
硬件异常模拟常伴随状态残留:临时文件未删、监听端口未释放、goroutine 泄漏。CI 环境下这些问题会被放大,导致后续测试 flaky。
- 所有测试必须加
-count=1,禁用 go test 缓存,防止前一次测试的 mock 状态污染下一次 - 使用
defer os.RemoveAll(tempDir)而非defer os.Remove,确保子目录也被清空 - 若启动了
httptest.Server或testcontainers,必须在defer中调用Close();若用了sync.WaitGroup或time.AfterFunc,需在测试结束前显式 cancel context - 最易忽略的是:mock 实现中忘记实现
io.Closer.Close()方法,或让它 panic —— 当被测代码调用Close()时,整个测试会中断,且错误堆栈指向 mock 内部而非业务代码
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










