在Go语言开发场景中,我们常常需要启动外部控制台应用,并且和它进行实时交互,比如向应用发送指令、读取应用返回的结果,而不是仅仅执行一次命令获取单次输出。这种交互式通信能力可以让我们把多个独立的可执行程序组合起来,实现更复杂的业务功能。

核心原理与依赖包
Go语言标准库中的os/exec包提供了启动和管理外部进程的能力,要实现交互式通信,核心是需要获取到外部进程的标准输入、标准输出和标准错误流,然后通过这些流进行双向数据传输。
当我们启动一个外部进程时,默认情况下它的输入输出会继承当前进程的终端,要实现程序内的交互,我们需要手动配置这些流的绑定方式,让Go程序可以直接操作它们。
基础实现步骤
1. 启动外部进程并配置流
首先我们需要使用exec.Command创建命令对象,然后通过StdinPipe方法获取标准输入流,通过StdoutPipe和StderrPipe获取标准输出和错误流,之后启动进程。
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"strings"
)
func main() {
// 启动一个外部交互式应用,这里以系统自带的cat命令为例,cat会读取输入并原样输出
cmd := exec.Command("cat")
// 获取标准输入流,用于向外部进程发送数据
stdin, err := cmd.StdinPipe()
if err != nil {
fmt.Println("获取标准输入流失败:", err)
return
}
// 获取标准输出流,用于读取外部进程的返回数据
stdout, err := cmd.StdoutPipe()
if err != nil {
fmt.Println("获取标准输出流失败:", err)
return
}
// 启动进程
if err := cmd.Start(); err != nil {
fmt.Println("启动进程失败:", err)
return
}
}
2. 实现双向数据交互
获取到输入输出流之后,我们可以使用bufio包提供的读写能力,实现向外部进程发送数据并读取返回结果的逻辑。下面的代码演示了向cat进程发送字符串,然后读取它的输出内容。
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"strings"
)
func main() {
cmd := exec.Command("cat")
stdin, err := cmd.StdinPipe()
if err != nil {
fmt.Println("获取标准输入流失败:", err)
return
}
stdout, err := cmd.StdoutPipe()
if err != nil {
fmt.Println("获取标准输出流失败:", err)
return
}
if err := cmd.Start(); err != nil {
fmt.Println("启动进程失败:", err)
return
}
// 向外部进程发送数据
sendData := "Hello, 外部控制台应用"
_, err = stdin.Write([]byte(sendData + "n"))
if err != nil {
fmt.Println("发送数据失败:", err)
return
}
// 读取外部进程的返回数据
reader := bufio.NewReader(stdout)
recvData, err := reader.ReadString('n')
if err != nil {
fmt.Println("读取数据失败:", err)
return
}
// 去除换行符并打印结果
fmt.Println("接收到的返回数据:", strings.TrimSpace(recvData))
// 关闭输入流,通知外部进程没有更多输入
stdin.Close()
// 等待进程结束
cmd.Wait()
}
3. 处理复杂交互场景
如果外部应用需要多轮交互,比如先发送用户名,再发送密码,我们可以根据外部应用的输出特征来判断交互阶段,动态发送对应的指令。下面的示例模拟了一个简单的多轮交互场景。
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"strings"
)
func main() {
// 这里以一个自定义的简单交互脚本为例,实际使用时替换为目标外部应用路径
// 假设外部脚本的交互逻辑是:先提示输入姓名,再提示输入年龄,最后返回汇总信息
cmd := exec.Command("bash", "-c", "read -p '请输入姓名: ' name; read -p '请输入年龄: ' age; echo 姓名:$name 年龄:$age")
stdin, err := cmd.StdinPipe()
if err != nil {
fmt.Println("获取标准输入流失败:", err)
return
}
stdout, err := cmd.StdoutPipe()
if err != nil {
fmt.Println("获取标准输出流失败:", err)
return
}
if err := cmd.Start(); err != nil {
fmt.Println("启动进程失败:", err)
return
}
reader := bufio.NewReader(stdout)
// 读取第一个提示语
line1, _ := reader.ReadString(':')
fmt.Println("外部应用提示:", strings.TrimSpace(line1))
// 发送姓名
stdin.Write([]byte("张三n"))
// 读取第二个提示语
line2, _ := reader.ReadString(':')
fmt.Println("外部应用提示:", strings.TrimSpace(line2))
// 发送年龄
stdin.Write([]byte("25n"))
// 读取最终结果
result, _ := reader.ReadString('n')
fmt.Println("最终结果:", strings.TrimSpace(result))
stdin.Close()
cmd.Wait()
}
注意事项
- 操作流的时候要注意关闭顺序,一般先关闭输入流,再等待进程退出,避免出现死锁。
- 读取输出流的时候要根据外部应用的实际输出格式选择合适的读取方式,比如按行读取或者按固定长度读取。
- 如果外部应用输出内容较多,建议使用带缓冲的读写器,避免频繁的系统调用影响性能。
- 处理错误的时候要全面,包括启动进程失败、获取流失败、读写数据失败等多种场景,保证程序的健壮性。
常见问题解答
为什么读取输出的时候会阻塞?
通常是因为外部进程还没有输出对应的内容,或者输出没有包含你指定的结束符,比如按行读取的时候外部进程没有输出换行符,就会导致读取阻塞。可以调整读取逻辑,或者确认外部应用的输出格式。
如何和外部应用传递二进制数据?
不需要使用bufio的按行读取,直接使用io.Read和io.Write方法,按照二进制流的格式进行读写即可,注意避免对二进制数据进行字符串相关的处理。