
本文讲解如何使用 Go 的 exec 包安全、可靠地捕获外部命令的标准输出与标准错误,避免因 goroutine 异步读取导致的数据丢失或不一致问题,并通过 sync.WaitGroup 实现同步等待。
本文讲解如何使用 go 的 `exec` 包安全、可靠地捕获外部命令的标准输出与标准错误,避免因 goroutine 异步读取导致的数据丢失或不一致问题,并通过 `sync.waitgroup` 实现同步等待。
在 Go 中调用外部命令(如 ls、curl 或 git)时,若需同时捕获 stdout 和 stderr 并可选地实时输出到终端,常见误区是启动 goroutine 读取管道后未等待其完成就直接调用 cmd.Run() —— 这会导致主流程可能在管道数据尚未全部读取完毕时就返回,从而造成输出截断、两次运行结果不一致等问题。
核心问题在于:cmd.Run() 仅等待进程退出,不保证关联的 io.Copy 操作已完成。而 io.Copy 在 goroutine 中异步执行,若未显式同步,output.String() 可能返回不完整内容。
✅ 正确做法是使用 sync.WaitGroup 确保所有管道读取 goroutine 完全结束后再返回结果。以下为推荐实现(支持 stdout/stderr 分离捕获与条件输出):
package main
import (
"bytes"
"io"
"log"
"os"
"os/exec"
"sync"
)
func run(cmd *exec.Cmd, displayOutput bool) (string, error) {
var output bytes.Buffer
var wg sync.WaitGroup
// 处理 stdout
stdout, err := cmd.StdoutPipe()
if err != nil {
return "", err
}
wg.Add(1)
go func() {
defer wg.Done()
if displayOutput {
io.Copy(io.MultiWriter(os.Stdout, &output), stdout)
} else {
io.Copy(&output, stdout)
}
}()
// 处理 stderr(独立缓冲,避免混入 stdout)
stderr, err := cmd.StderrPipe()
if err != nil {
return "", err
}
wg.Add(1)
go func() {
defer wg.Done()
if displayOutput {
io.Copy(io.MultiWriter(os.Stderr, &output), stderr)
} else {
io.Copy(&output, stderr)
}
}()
// 启动并等待命令完成
if err := cmd.Start(); err != nil {
return "", err
}
// 注意:必须先 wg.Wait(),再 cmd.Wait(),否则可能竞态
// 更佳实践:wg.Wait() 和 cmd.Wait() 并行等待,但确保 wg.Done() 在 Copy 结束后触发
// 因此我们在此处先启动 cmd,再等待 goroutines 完成,最后确认 cmd 已退出
if err := cmd.Wait(); err != nil {
// 即使 cmd 失败,仍需等待管道读取完成,防止 goroutine 泄漏
wg.Wait()
return output.String(), err
}
wg.Wait() // 确保所有 io.Copy 完全结束
return output.String(), nil
}
func main() {
// 示例:Windows 下使用 dir,Linux/macOS 可替换为 ls -lR /tmp
cmd1 := exec.Command("cmd", "/C", "dir", "/b", "C:\Windows\System32")
cmd2 := exec.Command("cmd", "/C", "dir", "/b", "C:\Windows\System32")
out1, err := run(cmd1, false)
if err != nil {
log.Fatal("First run failed:", err)
}
out2, err := run(cmd2, false)
if err != nil {
log.Fatal("Second run failed:", err)
}
if out1 == out2 {
log.Println("✅ Output consistency verified.")
} else {
log.Fatal("❌ Output mismatch detected!")
}
}
? 关键要点说明:
- ✅ sync.WaitGroup 是必须的:它确保 io.Copy 完全写入 bytes.Buffer 后才返回结果;
- ✅ cmd.Wait() 而非 cmd.Run():更清晰地区分启动与等待,便于错误处理;
- ✅ wg.Wait() 放在 cmd.Wait() 之后且独立调用:即使命令提前失败,也要等待管道读取完成,防止数据丢失或 goroutine 泄漏;
- ⚠️ 避免 go func() { ... }() 中直接调用 output.ReadFrom():该方法非并发安全,且不如 io.Copy 明确可控;
- ⚠️ 不要混合 stdout/stderr 到同一 MultiWriter(除非业务明确需要):否则日志顺序不可控;如需分离,可分别用两个 bytes.Buffer;
- ? 重复执行一致性保障:因完全同步,多次调用 run() 将始终返回相同结果,符合预期。
通过以上结构,你将获得稳定、可复现、线程安全的命令输出捕获能力,适用于日志采集、自动化脚本、CI/CD 工具链等生产场景。











