您现在的位置是:网站首页> 编程资料编程资料
Go语言中调用外部命令的方法总结_Golang_
2023-05-26
424人已围观
简介 Go语言中调用外部命令的方法总结_Golang_
引子
在工作中,我时不时地会需要在Go中调用外部命令。前段时间我做了一个工具,在钉钉群中添加了一个机器人,@这个机器人可以让它执行一些写好的脚本程序完成指定的任务。机器人倒是不难,照着钉钉开发者文档添加好机器人,然后@这个机器人就会向一个你指定的服务器发送一个POST请求,请求中会附带文本消息。所以我要做的就是搭一个Web服务器,可以用go原生的net/http包,也可以用gin/fasthttp/fiber这些Web框架。收到请求之后,检查附带文本中的关键字去调用对应的程序,然后返回结果。
go标准库中的os/exec包对调用外部程序提供了支持,本文详细介绍os/exec的使用姿势。
运行命令
Linux中有个cal命令,它可以显示指定年、月的日历,如果不指定年、月,默认为当前时间对应的年月。如果使用的是Windows,推荐安装msys2,这个软件包含了绝大多数的Linux常用命令。


那么,在Go代码中怎么调用这个命令呢?其实也很简单:
func main() { cmd := exec.Command("cal") err := cmd.Run() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) } } 首先,我们调用exec.Command传入命令名,创建一个命令对象exec.Cmd。接着调用该命令对象的Run()方法运行它。
如果你实际运行了,你会发现什么也没有发生,哈哈。事实上,使用os/exec执行命令,标准输出和标准错误默认会被丢弃。
显示输出
exec.Cmd对象有两个字段Stdout和Stderr,类型皆为io.Writer。我们可以将任意实现了io.Writer接口的类型实例赋给这两个字段,继而实现标准输出和标准错误的重定向。io.Writer接口在 Go 标准库和第三方库中随处可见,例如*os.File、*bytes.Buffer、net.Conn。所以我们可以将命令的输出重定向到文件、内存缓存甚至发送到网络中。
显示到标准输出
将exec.Cmd对象的Stdout和Stderr这两个字段都设置为os.Stdout,那么输出内容都将显示到标准输出:
func main() { cmd := exec.Command("cal") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) } } 运行程序。我在git bash运行,得到如下结果:

输出了中文,检查一下环境变量LANG的值,果然是zh_CN.UTF-8。如果想输出英文,可以将环境变量LANG设置为en_US.UTF-8:
$ echo $LANG zh_CN.UTF-8 $ LANG=en_US.UTF-8 go run main.go
得到输出:

输出到文件
打开或创建文件,然后将文件句柄赋给exec.Cmd对象的Stdout和Stderr这两个字段即可实现输出到文件的功能。
func main() { f, err := os.OpenFile("out.txt", os.O_WRONLY|os.O_CREATE, os.ModePerm) if err != nil { log.Fatalf("os.OpenFile() failed: %v\n", err) } cmd := exec.Command("cal") cmd.Stdout = f cmd.Stderr = f err = cmd.Run() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) } } os.OpenFile打开一个文件,指定os.O_CREATE标志让操作系统在文件不存在时自动创建一个,返回该文件对象*os.File。*os.File实现了io.Writer接口。
运行程序:
$ go run main.go
$ cat out.txt
November 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
发送到网络
现在我们来编写一个日历服务,接收年、月信息,返回该月的日历。
func cal(w http.ResponseWriter, r *http.Request) { year := r.URL.Query().Get("year") month := r.URL.Query().Get("month") cmd := exec.Command("cal", month, year) cmd.Stdout = w cmd.Stderr = w err := cmd.Run() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) } } func main() { http.HandleFunc("/cal", cal) http.ListenAndServe(":8080", nil) } 这里为了简单,错误处理都省略了。正常情况下,year和month参数都需要做合法性校验。exec.Command函数接收一个字符串类型的可变参数作为命令的参数:
func Command(name string, arg ...string) *Cmd
运行程序,使用浏览器请求localhost:8080/cal?year=2021&month=2得到:

保存到内存对象中
*bytes.Buffer同样也实现了io.Writer接口,故如果我们创建一个*bytes.Buffer对象,并将其赋给exec.Cmd的Stdout和Stderr这两个字段,那么命令执行之后,该*bytes.Buffer对象中保存的就是命令的输出。
func main() { buf := bytes.NewBuffer(nil) cmd := exec.Command("cal") cmd.Stdout = buf cmd.Stderr = buf err := cmd.Run() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) } fmt.Println(buf.String()) } 运行:
$ go run main.go
November 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
运行命令,然后得到输出的字符串或字节切片这种模式是如此的普遍,并且使用便利,os/exec包提供了一个便捷方法:CombinedOutput。
输出到多个目的地
有时,我们希望能输出到文件和网络,同时保存到内存对象。使用go提供的io.MultiWriter可以很容易实现这个需求。io.MultiWriter很方便地将多个io.Writer转为一个io.Writer。
我们稍微修改上面的web程序:
func cal(w http.ResponseWriter, r *http.Request) { year := r.URL.Query().Get("year") month := r.URL.Query().Get("month") f, _ := os.OpenFile("out.txt", os.O_CREATE|os.O_WRONLY, os.ModePerm) buf := bytes.NewBuffer(nil) mw := io.MultiWriter(w, f, buf) cmd := exec.Command("cal", month, year) cmd.Stdout = mw cmd.Stderr = mw err := cmd.Run() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) } fmt.Println(buf.String()) } 调用io.MultiWriter将多个io.Writer整合成一个io.Writer,然后将cmd对象的Stdout和Stderr都赋值为这个io.Writer。这样,命令运行时产出的输出会分别送往http.ResponseWriter、*os.File以及*bytes.Buffer。
运行命令,获取输出
前面提到,我们常常需要运行命令,返回输出。exec.Cmd对象提供了一个便捷方法:CombinedOutput()。该方法运行命令,将输出内容以一个字节切片返回便于后续处理。所以,上面获取输出的程序可以简化为:
func main() { cmd := exec.Command("cal") output, err := cmd.CombinedOutput() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) } fmt.Println(string(output)) } So easy!
CombinedOutput()方法的实现很简单,先将标准输出和标准错误重定向到*bytes.Buffer对象,然后运行程序,最后返回该对象中的字节切片:
func (c *Cmd) CombinedOutput() ([]byte, error) { if c.Stdout != nil { return nil, errors.New("exec: Stdout already set") } if c.Stderr != nil { return nil, errors.New("exec: Stderr already set") } var b bytes.Buffer c.Stdout = &b c.Stderr = &b err := c.Run() return b.Bytes(), err } CombinedOutput方法前几行判断表明,Stdout和Stderr必须是未设置状态。这其实很好理解,一般情况下,如果已经打算使用CombinedOutput方法获取输出内容,不会再自找麻烦地再去设置Stdout和Stderr字段了。
与CombinedOutput类似的还有Output方法,区别是Output只会返回运行命令产出的标准输出内容。
分别获取标准输出和标准错误
创建两个*bytes.Buffer对象,分别赋给exec.Cmd对象的Stdout和Stderr这两个字段,然后运行命令即可分别获取标准输出和标准错误。
func main() { cmd := exec.Command("cal", "15", "2012") var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) } fmt.Printf("output:\n%s\nerror:\n%s\n", stdout.String(), stderr.String()) } 标准输入
exec.Cmd对象有一个类型为io.Reader的字段Stdin。命令运行时会从这个io.Reader读取输入。先来看一个最简单的例子:
func main() { cmd := exec.Command("cat") cmd.Stdin = bytes.NewBufferString("hello\nworld") cmd.Stdout = os.Stdout err := cmd.Run() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) } } 如果不带参数运行cat命令,则进入交互模式,cat按行读取输入,并且原样发送到输出。
再来看一个复杂点的例子。Go标准库中compress/bzip2包只提供解压方法,并没有压缩方法。我们可以利用Linux命令bzip2实现压缩。bzip2从标准输入中读取数据,将其压缩,并发送到标准输出。
func bzipCompress(d []byte) ([]byte, error) { var out bytes.Buffer cmd := exec.Command("bzip2", "-c", "-9") cmd.Stdin = bytes.NewBuffer(d) cmd.Stdout = &out err := cmd.Run() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) } return out.Bytes(), nil } 参数-c表示压缩,-9表示压缩等级,9为最高。为了验证函数的正确性,写个简单的程序,先压缩"hello world"字符串,然后解压,看看是否能得到原来的字符串:
func main() { data := []byte("hello world") compressed, _ := bzipCompress(data) r := bzip2.NewReader(bytes.NewBuf
点击排行
本栏推荐
