
在 go 中使用 `exec.command` 调用 `diff` 时,程序因 `diff` 返回非零退出码(如文件不同返回 1)而报错,但这并非运行失败,而是 `diff` 的正常行为;需区分“执行失败”与“语义性差异结果”。
diff 命令的设计逻辑与其他工具(如 cat 或 wc)不同:它将「文件内容相同」视为成功(退出码 0),而「存在差异」或「文件不可读」分别返回 1 和 2。go 的 exec.Command(…).Output() 默认将任何非零退出码视为错误(*exec.ExitError),因此即使 diff -u a b 成功生成了标准输出,只要文件不一致,Go 就会触发 err != nil —— 这容易被误判为程序崩溃。
要正确处理,关键在于:
✅ 区分两类错误:
- *exec.ExitError:命令已执行,仅因语义原因(如差异存在、文件缺失)退出;
- 其他错误(如 exec.ErrNotFound、I/O 错误):命令根本未启动,属真正异常,需中止。
✅ 使用 CombinedOutput() 替代 Output():同时捕获 stdout 和 stderr,便于调试(例如当某文件不存在时,stderr 会输出 No such file or Directory)。
以下是推荐的健壮实现:
package main import ( "fmt" "log" "os/exec" "syscall" ) func main() { cmd := exec.Command("diff", "-u", "/tmp/revision-1", "/tmp/revision-4") output, err := cmd.CombinedOutput() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { // 检查具体退出状态 if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { code := status.ExitStatus() switch code { case 0: fmt.Println("✅ 文件完全相同") case 1: fmt.Println("⚠️ 文件存在差异(正常行为)") fmt.Println(string(output)) // 输出 unified diff 内容 case 2: log.Fatalf("❌ diff 执行失败(如文件不存在、权限不足): %s", string(output)) default: log.Fatalf("❌ 未知退出码 %d: %s", code, string(output)) } } else { log.Fatalf("❌ 无法解析系统退出状态: %v", err) } } else { // 非 ExitError,例如命令未找到、路径错误等 log.Fatalf("❌ 命令执行异常: %v", err) } } else { // exit code == 0,无差异 fmt.Println("✅ 文件完全相同") } }
注意事项:
- syscall.WaitStatus 在 linux/macOS 下有效,但 windows 不兼容(需用 exitErr.ExitCode());若需跨平台,请用 runtime.GOOS 分支处理;
- 确保待比较文件路径真实存在且有读取权限,否则 diff 会以退出码 2 报错;
- 若仅需判断是否相等(不关心 diff 内容),可改用 os.SameFile 或 bytes.Equal(io.ReadAll(f1), io.ReadAll(f2)),更高效且无 shell 依赖。
总结:diff 的退出码是其接口契约的一部分,而非错误信号。Go 的 exec 包严格遵循 POSIX 语义,开发者需主动解包 ExitError 并按业务逻辑解读退出码,而非一概 log.Fatal。