正确做法是将命令名与各参数拆分为独立字符串,如exec.Command("ls", "-l", "/tmp");参数含空格或特殊字符时无需转义,Go会原样安全传递;Output()合并stdout/stderr且退出码非0即报错,需注意解析返回值。

用 exec.Command 启动命令,别直接拼字符串
Go 不允许像 shell 那样用字符串拼接命令行执行,必须把命令和参数拆成独立的 []string。否则遇到含空格、引号或特殊字符的参数(比如文件路径 /path/with space/file.txt)会出错,exec.Command("ls -l /tmp") 这种写法根本不会按预期运行——它会试图找一个叫 "ls -l /tmp" 的可执行文件。
正确做法是:
cmd := exec.Command("ls", "-l", "/tmp")
如果参数本身来自用户输入或变量,直接传入即可,exec.Command 会安全处理它们,不需要手动转义或包裹引号。
用 cmd.Output() 拿 stdout,但注意它会同时捕获 stderr 并在失败时返回错误
cmd.Output() 是最常用的快捷方式,但它有个关键行为:只要子进程退出码非 0,哪怕 stdout 有内容,它也会返回非 nil 错误,并把 stderr 和 stdout 合并进返回的字节切片里。这容易让人误以为“命令失败就没输出”,其实可能只是 stderr 被混进来了。
常见误区:
- 看到
exit status 1错误就放弃读取返回值,错过有用的 stdout - 没意识到
Output()内部调用了Run(),不能重复调用
更可控的做法是显式设置输出目标:
var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() // err != nil 不代表 stdout 为空,先检查 stdout.String()
需要区分 stdout/stderr 或处理大输出时,避免用 Output(),改用 StdoutPipe + Start + Wait
当命令输出很大(比如 tar -cf - /usr),或者你想边读边处理、做超时控制、或严格分离 stdout/stderr,就得绕过 Output() 和 CombinedOutput()。
关键点:
- 必须先调用
cmd.Start(),再读 pipe,否则会死锁 - pipe 是阻塞的,如果只读 stdout 却不读 stderr,而命令又往 stderr 写了大量数据,进程会卡住
- 记得调用
cmd.Wait()等待完成,否则可能漏掉最后一点输出
简例:
cmd := exec.Command("find", "/tmp", "-name", "*.log")
stdout, _ := cmd.StdoutPipe()
cmd.Start()
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
fmt.Println(scanner.Text()) // 实时打印每一行
}
cmd.Wait() // 必须等,否则可能丢数据
超时、环境变量、工作目录这些控制项得在 cmd 实例上设,不是传参
很多人想当然地把超时加到命令参数里(比如 exec.Command("timeout", "5s", "curl", "...")),这依赖系统有 timeout 命令,且行为不一致(macOS 没原生 timeout)。Go 自带的超时机制更可靠,但必须通过 context 设置:
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "curl", "https://httpbin.org/delay/5")
其他常用配置:
-
cmd.Dir = "/home/user/project"指定工作目录 -
cmd.Env = append(os.Environ(), "API_KEY=xxx")注入环境变量(注意要带上原始环境) -
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}在 Linux 上控制进程组(比如想 kill 整个子树)
这些都得在 cmd.Run() 或 Start() 之前设置,之后改无效。


















