用 os.Pipe 重定向 os.Stdout 捕获输出需先保存原值、替换为 pipeWriter、关闭 writer 后读取,测试后必须恢复;log 输出用 log.SetOutput 更安全;同时捕获多流需独立管道并严格配对关闭与还原。

用 os.Pipe 重定向 os.Stdout 捕获打印内容
Go 测试中想验证 fmt.Println 或其他直接写 os.Stdout 的输出,不能靠修改全局变量“打补丁”,必须在测试前临时替换 os.Stdout 为管道读写端。关键点是:替换后要记得恢复,否则影响后续测试。
常见错误是只调用 os.Stdout = pipeWriter,但没等写入完成就读取,导致读到空字符串;或者忘记恢复,让别的测试也写进同一管道里,结果串行错乱。
- 先调用
os.Pipe()得到reader和writer - 保存原始
os.Stdout,再赋值为writer - 运行待测函数(它会往
os.Stdout写) - 关闭
writer(否则io.ReadAll会阻塞) - 用
io.ReadAll(reader)获取全部输出 - 测试结束后,务必把
os.Stdout设回原值
用 log.SetOutput 拦截标准日志输出
log.Printf 等默认输出到 os.Stderr,但 Go 的 log 包提供 log.SetOutput(io.Writer) 接口,比动 os.Stderr 更安全、更精准。它只影响你显式调用的 log 实例(包括默认的 log 包变量),不影响第三方库或 fmt 输出。
容易踩的坑是:测试里调了 log.SetOutput 却没在 defer 中还原,导致后续测试的日志消失或错位;还有人误以为设了 SetOutput 就能捕获 logrus 或 zap 的日志——不行,那些是独立日志库,得各自处理。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 创建
bytes.Buffer或os.Pipe作为接收器 - 调用
log.SetOutput(buffer) - 执行触发日志的代码
- 用
buffer.String()检查内容 - 在测试函数开头或
defer里调用log.SetOutput(os.Stderr)还原
测试中同时捕获 stdout 和 stderr 的典型组合写法
很多 CLI 工具既用 fmt.Fprintln(os.Stdout, ...) 又用 log.Printf(...),还可能出错时写 os.Stderr。这时候需要分别接管三者,且顺序和清理必须严格匹配,否则读取混乱或死锁。
一个可靠做法是:对每个要捕获的流,都用独立 os.Pipe,并在 defer 中统一关闭 writer + 恢复原流。别图省事共用一个管道——stdout 和 stderr 是两个独立 fd,混在一起无法区分来源。
- 分别调用三次
os.Pipe(),得到三组 reader/writer - 保存
os.Stdout、os.Stderr、log.Writer()(或直接记下原os.Stderr) - 按需赋值:
os.Stdout = stdoutWriter,os.Stderr = stderrWriter,log.SetOutput(logWriter) - 执行被测逻辑
- 依次关闭三个 writer(注意:不关会阻塞读)
- 用
io.ReadAll分别读取三个 reader
用 testify/assert 配合捕获做断言时要注意什么
如果你用 testify/assert 做输出校验,比如 assert.Contains(t, stdoutStr, "expected"),得注意字符串里可能含换行符、空格缩进、颜色转义序列(如果用了带 color 的日志库)。直接比 == 很容易失败。
另一个易忽略点是:捕获内容是 []byte,转成 string 后末尾常带 \n,而你的期望字符串没加,导致断言失败。建议统一用 strings.TrimSpace 处理后再比对。
- 捕获后优先用
strings.TrimSpace去首尾空白 - 检查是否含 ANSI 转义(如
\x1b[32mOK\x1b[0m),必要时用stripansi类库清洗 - 避免用
assert.Equal直接比完整输出,改用assert.Contains或正则匹配关键片段 - 若测试并发执行,确保每个测试用独立管道,不要复用全局 buffer
defer log.SetOutput(os.Stderr),就可能让整个测试套件的日志静默失效。

















