os.pipe不能直接捕获cmd.stdout/stderr,因其返回的读端需接收数据,而cmd.stdout/stderr只接受写端;正确做法是将os.pipe的写端赋给二者,并从读端读取。

为什么 os.Pipe 不能直接捕获 cmd.Stdout 和 cmd.Stderr?
因为 cmd.Stdout 和 cmd.Stderr 是写入目标,而 os.Pipe 返回的是成对的 *os.File:一端可读(用于接收输出),一端可写(需赋给子进程)。你不能把读端直接塞进 cmd.Stdout——它只接受写端。常见错误是搞反方向,导致子进程 panic 或阻塞。
正确做法是:用 os.Pipe() 创建管道,把写端(wr)赋给 cmd.Stdout 和/或 cmd.Stderr,然后在父进程中从读端(rd)读取数据。
- 子进程写入
wr,父进程从rd读出 - 必须在
cmd.Start()前完成赋值,否则无效 - 若同时捕获 stdout 和 stderr,建议分别创建两个 pipe,避免混流和竞态
如何用两个 pipe 分别捕获 stdout 和 stderr?
这是最稳妥的方式,尤其当你要区分日志级别、做不同解析或防止 stderr 冲刷 stdout 缓冲时。注意:两个 pipe 的读端需并发读取,否则可能因某一方阻塞导致整个子进程挂起(比如 stderr 满了但没人读)。
stdoutRd, stdoutWr, _ := os.Pipe()
stderrRd, stderrWr, _ := os.Pipe()
cmd := exec.Command("sh", "-c", "echo 'hello'; echo 'error' >&2")
cmd.Stdout = stdoutWr
cmd.Stderr = stderrWr
_ = cmd.Start()
// 立即关闭写端,否则 Read 会一直等 EOF
stdoutWr.Close()
stderrWr.Close()
// 并发读取,避免阻塞
var stdoutBuf, stderrBuf bytes.Buffer
var wg sync.WaitGroup
wg.Add(2)
go func() {
io.Copy(&stdoutBuf, stdoutRd)
stdoutRd.Close()
wg.Done()
}()
go func() {
io.Copy(&stderrBuf, stderrRd)
stderrRd.Close()
wg.Done()
}()
wg.Wait()
fmt.Println("stdout:", stdoutBuf.String()) // "hello\n"
fmt.Println("stderr:", stderrBuf.String()) // "error\n"
能否复用同一个 pipe 捕获 stdout 和 stderr?
可以,但不推荐。把同一对 pipe 的写端同时赋给 cmd.Stdout 和 cmd.Stderr,会导致输出混在一起,且无法区分来源。更严重的是:如果子进程先写大量 stderr,而父进程还没开始读,pipe 缓冲区满后子进程会阻塞在 write 系统调用上——即使 stdout 还没写,整个命令也卡住。
- 混流场景仅适用于调试或简单脚本,且子进程输出量极小
- 必须确保读端及时消费,可用
io.MultiReader合并多个io.Reader,但依然无法溯源 - Linux 下可通过
2>&1在 shell 层合并,但那是子进程自己的行为,和 Go 的 pipe 复用无关
容易被忽略的资源清理和错误处理点
pipe 文件描述符泄漏和 goroutine 泄漏是真实高频问题。很多人只记得 close 读端,却忘了 close 写端;或用 io.Copy 读取但没处理子进程提前退出导致的 EOF,造成 goroutine 永久等待。
-
cmd.Wait()必须调用,否则子进程变成僵尸;它也会触发写端自动关闭(但不保证立即) - 务必在
cmd.Start()后、cmd.Wait()前显式Close()所有 pipe 写端,否则io.Copy不会返回 - 读取时用
io.ReadFull或带超时的io.LimitReader防止无限等待大输出 - 若子进程崩溃,
cmd.Wait()返回非 nil error,此时 pipe 读端可能已断开,需检查io.ErrUnexpectedEOF等情况
真正麻烦的从来不是创建 pipe,而是确保读、写、wait、close 四个动作的时序和配对不出错。多一个 goroutine 就多一分泄漏风险,少一次 close 就多一个 fd 占用。











