在工作中,我时不时地会需要在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