在Go編程中調(diào)用外部命令的幾種場(chǎng)景
在很多場(chǎng)合, 使用Go語(yǔ)言需要調(diào)用外部命令來(lái)完成一些特定的任務(wù), 例如: 使用Go語(yǔ)言調(diào)用Linux命令來(lái)獲取執(zhí)行的結(jié)果,又或者調(diào)用第三方程序執(zhí)行來(lái)完成額外的任務(wù)。在go的標(biāo)準(zhǔn)庫(kù)中, 專門提供了os/exec包來(lái)對(duì)調(diào)用外部程序提供支持, 本文將對(duì)調(diào)用外部命令常用的幾種場(chǎng)景進(jìn)行總結(jié)。

直接調(diào)用函數(shù)
先用Linux上的一個(gè)簡(jiǎn)單命令執(zhí)行看一下效果, 執(zhí)行cal命令, 會(huì)打印當(dāng)前月的日期信息,如圖:

如果要使用Go代碼調(diào)用該命令, 可以使用以下代碼:
func main(){
  cmd := exec.Command("cal")
  err := cmd.Run()
  if err != nil {
     fmt.Println(err.Error())
  }
}首先, 調(diào)用"os/exec"包中的Command函數(shù),并傳入命令名稱作為參數(shù), Command函數(shù)會(huì)返回一個(gè)exec.Cmd的命令對(duì)象。接著調(diào)用該命令對(duì)象的Run()方法運(yùn)行命令。
如果此時(shí)運(yùn)行程序, 會(huì)發(fā)現(xiàn)什么都沒(méi)有出現(xiàn), 這是因?yàn)槲覀儧](méi)有處理標(biāo)準(zhǔn)輸出, 調(diào)用os/exec執(zhí)行命令, 標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤默認(rèn)會(huì)被丟棄。
這里將cmd結(jié)構(gòu)中的Stdout和Stderr分別設(shè)置為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())
    }
}運(yùn)行程序后顯示:

輸出到文件
輸出到文件的關(guān)鍵, 是將exec.Cmd對(duì)象的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打開(kāi)一個(gè)文件, 指定os.0_CREATE標(biāo)志讓操作系統(tǒng)在文件不存在時(shí)自動(dòng)創(chuàng)建, 返回文件對(duì)象*os.File, *os.File實(shí)現(xiàn)了io.Writer接口。
運(yùn)行程序結(jié)果如下:

發(fā)送到網(wǎng)絡(luò)
這里開(kāi)啟一個(gè)HTTP服務(wù), 服務(wù)端接收兩個(gè)參數(shù):年和月, 在服務(wù)端通過(guò)執(zhí)行系統(tǒng)命令返回結(jié)果,代碼如下:
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)
}打開(kāi)瀏覽器,在地址欄中輸入U(xiǎn)RL查詢2023年10月份的日歷:http://localhost:8001/querydate?year=2023&mnotallow=10 , 結(jié)果如下:

輸出到多個(gè)目標(biāo)
如果要將執(zhí)行命令的結(jié)果同時(shí)輸出到文件、網(wǎng)絡(luò)和內(nèi)存對(duì)象, 可以使用io.MultiWriter滿足需求, io.MultiWriter可以很方便的將多個(gè)io.Writer轉(zhuǎn)換成一個(gè)io.Writer, 修改之前的Web服務(wù)端程序如下:
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)
}分別獲取輸出內(nèi)容和錯(cuò)誤
這里我們封裝一個(gè)常用函數(shù), 輸入接收命令和多個(gè)參數(shù), 返回錯(cuò)誤和命令返回信息, 函數(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í)行返回結(jié)果的函數(shù), 分別返回了錯(cuò)誤和命令返回信息。
循環(huán)獲取命令內(nèi)容
在Linux系統(tǒng)中,有些命令運(yùn)行后結(jié)果是動(dòng)態(tài)持續(xù)更新的,例如: top命令,對(duì)于該場(chǎng)景,我們封裝函數(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
}通過(guò)調(diào)用cmd對(duì)象的StdoutPipe()輸出管理函數(shù), 我們可以實(shí)現(xiàn)持續(xù)獲取后臺(tái)命令返回的結(jié)果,并保持程序不退出。
在調(diào)用該函數(shù)的時(shí)候, 調(diào)用方式如下:
<-ExecCommandLoopTimeOutput("top")打印出的信息將是一個(gè)持續(xù)顯示信息,如圖:

總結(jié)
本章節(jié)介紹了使用os/exec這個(gè)標(biāo)準(zhǔn)庫(kù)調(diào)用外部命令的各種場(chǎng)景。在實(shí)際應(yīng)用中, 基本用的最多的還是封裝好的:ExecCommandOneTimeOutput()和ExecCommandLoopTimeOutput()兩個(gè)函數(shù), 畢竟外部命令一般只會(huì)包含兩種:一種是執(zhí)行后馬上獲取結(jié)果,第二種就是常駐內(nèi)存持續(xù)獲取結(jié)果。















 
 
 









 
 
 
 