在Go編程中調用外部命令的幾種場景
在很多場合, 使用Go語言需要調用外部命令來完成一些特定的任務, 例如: 使用Go語言調用Linux命令來獲取執(zhí)行的結果,又或者調用第三方程序執(zhí)行來完成額外的任務。在go的標準庫中, 專門提供了os/exec包來對調用外部程序提供支持, 本文將對調用外部命令常用的幾種場景進行總結。
直接調用函數(shù)
先用Linux上的一個簡單命令執(zhí)行看一下效果, 執(zhí)行cal命令, 會打印當前月的日期信息,如圖:
如果要使用Go代碼調用該命令, 可以使用以下代碼:
func main(){
cmd := exec.Command("cal")
err := cmd.Run()
if err != nil {
fmt.Println(err.Error())
}
}
首先, 調用"os/exec"包中的Command函數(shù),并傳入命令名稱作為參數(shù), Command函數(shù)會返回一個exec.Cmd的命令對象。接著調用該命令對象的Run()方法運行命令。
如果此時運行程序, 會發(fā)現(xiàn)什么都沒有出現(xiàn), 這是因為我們沒有處理標準輸出, 調用os/exec執(zhí)行命令, 標準輸出和標準錯誤默認會被丟棄。
這里將cmd結構中的Stdout和Stderr分別設置為os.stdout和os.Stderr, 代碼如下:
func main(){
cmd := exec.Command("cal")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
fmt.Println(err.Error())
}
}
運行程序后顯示:
輸出到文件
輸出到文件的關鍵, 是將exec.Cmd對象的Stdout和Stderr賦值文件句柄, 代碼如下:
func main(){
f, err := os.OpenFile("sample.txt", os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
fmt.Println(err.Error())
}
cmd := exec.Command("cal")
cmd.Stdout = f
cmd.Stderr = f
err := cmd.Run()
if err != nil {
fmt.Println(err.Error())
}
}
os.OpenFile打開一個文件, 指定os.0_CREATE標志讓操作系統(tǒng)在文件不存在時自動創(chuàng)建, 返回文件對象*os.File, *os.File實現(xiàn)了io.Writer接口。
運行程序結果如下:
發(fā)送到網絡
這里開啟一個HTTP服務, 服務端接收兩個參數(shù):年和月, 在服務端通過執(zhí)行系統(tǒng)命令返回結果,代碼如下:
import (
"fmt"
"net/http"
"os/exec"
)
func queryDate(w http.ResponseWriter, r *http.Request) {
var err error
if r.Method == "GET" {
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 {
fmt.Println(err.Error())
}
}
}
func main() {
http.HandleFunc("/querydate", queryDate)
http.ListenAndServe(":8001", nil)
}
打開瀏覽器,在地址欄中輸入URL查詢2023年10月份的日歷:http://localhost:8001/querydate?year=2023&mnotallow=10 , 結果如下:
輸出到多個目標
如果要將執(zhí)行命令的結果同時輸出到文件、網絡和內存對象, 可以使用io.MultiWriter滿足需求, io.MultiWriter可以很方便的將多個io.Writer轉換成一個io.Writer, 修改之前的Web服務端程序如下:
func queryDate(w http.ResponseWriter, r *http.Request) {
var err error
if r.Method == "GET" {
buffer := bytes.NewBuffer(nil)
year := r.URL.Query().Get("year")
month := r.URL.Query().Get("month")
f, _ := os.OpenFile("sample.txt", os.O_WRONLY|os.O_CREATE, os.ModePerm)
mw := io.MultiWriter(w, f, buffer)
cmd := exec.Command("cal", month, year)
cmd.Stdout = mw
cmd.Stderr = mw
err = cmd.Run()
if err != nil {
fmt.Println(err.Error())
}
fmt.Println(buffer.String())
}
}
func main() {
http.HandleFunc("/querydate", queryDate)
http.ListenAndServe(":8001", nil)
}
分別獲取輸出內容和錯誤
這里我們封裝一個常用函數(shù), 輸入接收命令和多個參數(shù), 返回錯誤和命令返回信息, 函數(shù)代碼如下:
func ExecCommandOneTimeOutput(name string, args ...string) (error, string) {
var out bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command(name, args...)
cmd.Stdout = &out
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
fmt.Println(fmt.Sprint(err) + ": " + stderr.String())
return err, ""
}
return nil, out.String()
}
該函數(shù)可以作為通用的命令執(zhí)行返回結果的函數(shù), 分別返回了錯誤和命令返回信息。
循環(huán)獲取命令內容
在Linux系統(tǒng)中,有些命令運行后結果是動態(tài)持續(xù)更新的,例如: top命令,對于該場景,我們封裝函數(shù)如下:
func ExecCommandLoopTimeOutput(name string, args ...string) <-chan struct{} {
cmd := exec.Command(name, args...)
closed := make(chan struct{})
defer close(closed)
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
fmt.Println(err.Error())
}
defer stdoutPipe.Close()
go func() {
scanner := bufio.NewScanner(stdoutPipe)
for scanner.Scan() {
fmt.Println(string(scanner.Bytes()))
_, err := simplifiedchinese.GB18030.NewDecoder().Bytes(scanner.Bytes())
if err != nil {
continue
}
}
}()
if err := cmd.Run(); err != nil {
fmt.Println(err.Error())
}
return closed
}
通過調用cmd對象的StdoutPipe()輸出管理函數(shù), 我們可以實現(xiàn)持續(xù)獲取后臺命令返回的結果,并保持程序不退出。
在調用該函數(shù)的時候, 調用方式如下:
<-ExecCommandLoopTimeOutput("top")
打印出的信息將是一個持續(xù)顯示信息,如圖:
總結
本章節(jié)介紹了使用os/exec這個標準庫調用外部命令的各種場景。在實際應用中, 基本用的最多的還是封裝好的:ExecCommandOneTimeOutput()和ExecCommandLoopTimeOutput()兩個函數(shù), 畢竟外部命令一般只會包含兩種:一種是執(zhí)行后馬上獲取結果,第二種就是常駐內存持續(xù)獲取結果。