
本文详解如何在 go 中使用 os/exec 实现命令间标准输出(stdout)到标准输入(stdin)的管道传递,包括启动子进程、建立管道连接、流式处理与错误管理,并给出安全可靠的完整示例。
本文详解如何在 go 中使用 os/exec 实现命令间标准输出(stdout)到标准输入(stdin)的管道传递,包括启动子进程、建立管道连接、流式处理与错误管理,并给出安全可靠的完整示例。
在 Go 中,exec.Command 仅构建命令对象,不会自动执行或建立进程间管道。若需将一个命令(如 mdb-export)的输出直接传给另一个命令(如 psql),必须手动连接其 Stdout 与 Stdin,并确保进程生命周期可控。以下为专业、健壮的实现方案。
✅ 正确做法:显式创建管道并复用 io.Pipe
最可靠的方式是使用 io.Pipe() 创建内存管道,将前一命令的 Stdout 写入管道,再将后一命令的 Stdin 从该管道读取。这种方式避免竞态、支持流式传输,且无需临时文件或 shell 解析。
package main
import (
"bufio"
"io"
"log"
"os/exec"
)
func main() {
// Step 1: 获取所有表名(-1 表示每行一个表)
tablesCmd := exec.Command("mdb-tables", "-1", "myaccessdatabase.mdb")
tablesOut, err := tablesCmd.Output()
if err != nil {
log.Fatalf("failed to list tables: %v, output: %s", err, string(tablesOut))
}
// Step 2: 遍历每个表,构建并执行管道链:mdb-export → psql
scanner := bufio.NewScanner(&tablesOut)
for scanner.Scan() {
table := scanner.Text()
if table == "" {
continue
}
// 构建 mdb-export 命令(注意:-q '\'' 中的单引号需转义为 '\'',实际应为 -q "'")
exportCmd := exec.Command("mdb-export", "-I", "postgres", "-q", "'", "myaccessdatabase.mdb", table)
// 构建 psql 命令
psqlCmd := exec.Command("psql", "-d", "mypsqldatabase", "-U", "postgres", "-w", "-h", "localhost")
// 创建管道:exportCmd.Stdout → psqlCmd.Stdin
pr, pw := io.Pipe()
exportCmd.Stdout = pw
psqlCmd.Stdin = pr
// 捕获错误(重要!)
var exportErr, psqlErr error
exportCmd.Stderr = &exportErr
psqlCmd.Stderr = &psqlErr
// 启动两个命令(注意顺序:先启动 psql,再启动 export,避免死锁)
if err := psqlCmd.Start(); err != nil {
log.Printf("failed to start psql for table %s: %v", table, err)
continue
}
if err := exportCmd.Start(); err != nil {
log.Printf("failed to start mdb-export for table %s: %v", table, err)
psqlCmd.Process.Kill() // 清理已启动的 psql
continue
}
// 等待 export 完成(它会关闭 pw,从而通知 psql EOF)
if err := exportCmd.Wait(); err != nil {
pw.CloseWithError(err) // 显式关闭并传播错误
log.Printf("mdb-export failed for table %s: %v", table, err)
psqlCmd.Process.Kill()
continue
}
pw.Close() // 正常关闭写端,让 psql 读取完毕后退出
// 等待 psql 完成
if err := psqlCmd.Wait(); err != nil {
log.Printf("psql failed for table %s: %v", table, err)
continue
}
log.Printf("✅ Successfully imported table: %s", table)
}
if err := scanner.Err(); err != nil {
log.Fatal("error scanning table names:", err)
}
}⚠️ 关键注意事项
- 不要依赖 cmd.Output() + strings.Split 处理表名:mdb-tables -1 输出可能含空行、空格或特殊字符,应始终用 bufio.Scanner 安全逐行解析。
- 避免 cmd.Run() 后再调 cmd.StdoutPipe():Run() 会阻塞并消耗 stdout,后续 StdoutPipe() 将返回 nil 或 panic。
- 必须显式关闭管道写端(pw.Close()):否则 psql 会永远等待输入,造成死锁。
- 错误处理不可省略:psql 可能因权限、连接或 SQL 错误失败;务必检查 Wait() 返回值,并在出错时清理子进程(如 Kill())。
- 参数转义要严谨:mdb-export -q "'" 中的单引号在 Go 字符串中写作 "'",而非 "\'"(后者在 shell 中才等效)。
- 生产环境强烈建议替代方案:如答案所提,直接使用 database/sql + PostgreSQL 驱动(如 github.com/lib/pq 或 github.com/jackc/pgx)导入数据更安全、可控、可调试,且规避 shell 注入风险。
✅ 总结
Go 的 os/exec 支持完全可控的进程管道,但需手动管理 Stdin/Stdout 连接、生命周期与错误传播。核心模式为:
- 创建 io.Pipe();
- 将前命令 Stdout 设为 PipeWriter,后命令 Stdin 设为 PipeReader;
- 先 Start() 后命令(消费者),再 Start() 前命令(生产者);
- Wait() 前命令后 Close() 写端,最后 Wait() 后命令。
此模式适用于任何需要命令链式流式处理的场景(如 grep | awk | sort),是 Go 系统编程的必备实践。


















