本文详解 go 语言中使用 `os/exec` 启动子进程并实时、逐行读取其标准输出的完整实践,涵盖管道初始化、错误处理、标准错误重定向、goroutine 同步等关键要点。
在 Go 中通过 exec.Command 启动外部命令并读取其输出是常见需求,但若未正确处理管道、错误流或生命周期同步,极易出现“程序卡住”“无输出”或“数据丢失”等问题。你提供的代码看似逻辑清晰,却始终无法触发 scanner.Scan(),根本原因在于三个被忽略的关键环节:错误未检查、stderr 被静默丢弃、以及 cmd.Wait() 过早调用引发竞态。
cmd.StdoutPipe() 可能失败(例如命令未设置 cmd.Stdout = nil 时重复调用),必须显式校验:
out, err := cmd.StdoutPipe() if err != nil { log.Fatal("Failed to get stdout pipe:", err) }
pocketsphinx_continuous 在缺少必要参数(如 -hmm、-dict)时不会向 stdout 输出任何内容,而是将错误直接写入 stderr。而你的代码完全忽略了 stderr,导致“看似运行但无响应”。务必同时捕获 stderr 进行调试:
stderr, err := cmd.StderrPipe()
if err != nil {
log.Fatal("Failed to get stderr pipe:", err)
}
// 启动 goroutine 实时打印 stderr(开发阶段强烈推荐)
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
log.Printf("[ERR] %s", scanner.Text())
}
}()? 提示:生产环境可将 stderr 重定向至日志文件,但开发阶段务必实时查看——这是定位 pocketsphinx 类工具启动失败的首要线索。
pocketsphinx_continuous 是一个严格依赖声学模型与词典的语音识别引擎。以下是最小可用命令示例(路径需按实际调整):
cmd := exec.Command(
"/usr/local/bin/pocketsphinx_continuous",
"-inmic", "yes",
"-hmm", "/usr/local/share/pocketsphinx/model/en-us/en-us",
"-dict", "/usr/local/share/pocketsphinx/model/en-us/cmudict-en-us.dict",
"-lm", "/usr/local/share/pocketsphinx/model/en-us/en-us.lm.bin",
)缺少任一模型参数,进程会立即退出,stdout 为空,stderr 报错(如 FATAL_ERROR: "acmod.c", line 142: Failed to open model definition)。
defer cmd.Wait() 在 main() 返回前才执行,但此时 readStuff goroutine 可能尚未结束,导致 cmd.Wait() 提前阻塞或子进程被意外终止。必须等待扫描完成后再调用 Wait():
func readStuff(scanner *bufio.Scanner, done chan<- bool) {
defer close(done) // 通知主协程扫描结束
for scanner.Scan() {
fmt.Println("→", scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Printf("Scanner error: %v", err)
}
}
// 主函数中:
done := make(chan bool)
go readStuff(scanner, done)
<-done // 阻塞等待扫描完成
if err := cmd.Wait(); err != nil {
log.Printf("Command finished with error: %v", err)
}package main
import (
"bufio"
"log"
"os/exec"
"time"
)
func main() {
cmd := exec.Command(
"/usr/local/bin/pocketsphinx_continuous",
"-inmic", "yes",
"-hmm", "/usr/local/share/pocketsphinx/model/en-us/en-us",
"-dict", "/usr/local/share/pocketsphinx/model/en-us/cmudict-en-us.dict",
"-lm", "/usr/local/share/pocketsphinx/model/en-us/en-us.lm.bin",
)
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal("StdoutPipe failed:", err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
log.Fatal("StderrPipe failed:", err)
}
// 启动 stderr 监听(调试关键!)
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
log.Printf("[SPHINX-ERR] %s", scanner.Text())
}
}()
if err := cmd.Start(); err != nil {
log.Fatal("Cmd start failed:", err)
}
// 启动 stdout 处理
done := make(chan bool)
go func() {
defer close(done)
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
log.Printf("[SPHINX-OUT] %s", scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Printf("Scanner error: %v", err)
}
}()
// 等待处理完成(或设超时避免永久阻塞)
select {
case <-done:
log.Println("Output processing completed.")
case <-time.After(30 * time.Second):
log.Println("Timeout waiting for output; terminating...")
cmd.Process.Kill()
}
if err := cmd.Wait(); err != nil {
log.Printf("Process exited with error: %v", err)
}
}遵循以上原则,即可稳定、可靠地从任意子进程(不限于 pocketsphinx)中流式读取标准输出。