最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Go語言中調(diào)用外部命令的方法總結(jié)

 更新時間:2022年11月03日 08:44:44   作者:darjun  
在工作中,我們時不時地會需要在Go中調(diào)用外部命令。本文為大家總結(jié)了Go語言中調(diào)用外部命令的幾種姿勢,感興趣的小伙伴可以跟隨小編一起學習一下

引子

在工作中,我時不時地會需要在Go中調(diào)用外部命令。前段時間我做了一個工具,在釘釘群中添加了一個機器人,@這個機器人可以讓它執(zhí)行一些寫好的腳本程序完成指定的任務(wù)。機器人倒是不難,照著釘釘開發(fā)者文檔添加好機器人,然后@這個機器人就會向一個你指定的服務(wù)器發(fā)送一個POST請求,請求中會附帶文本消息。所以我要做的就是搭一個Web服務(wù)器,可以用go原生的net/http包,也可以用gin/fasthttp/fiber這些Web框架。收到請求之后,檢查附帶文本中的關(guān)鍵字去調(diào)用對應(yīng)的程序,然后返回結(jié)果。

go標準庫中的os/exec包對調(diào)用外部程序提供了支持,本文詳細介紹os/exec的使用姿勢。

運行命令

Linux中有個cal命令,它可以顯示指定年、月的日歷,如果不指定年、月,默認為當前時間對應(yīng)的年月。如果使用的是Windows,推薦安裝msys2,這個軟件包含了絕大多數(shù)的Linux常用命令。

那么,在Go代碼中怎么調(diào)用這個命令呢?其實也很簡單:

func main() {
  cmd := exec.Command("cal")
  err := cmd.Run()
  if err != nil {
    log.Fatalf("cmd.Run() failed: %v\n", err)
  }
}

首先,我們調(diào)用exec.Command傳入命令名,創(chuàng)建一個命令對象exec.Cmd。接著調(diào)用該命令對象的Run()方法運行它。

如果你實際運行了,你會發(fā)現(xiàn)什么也沒有發(fā)生,哈哈。事實上,使用os/exec執(zhí)行命令,標準輸出和標準錯誤默認會被丟棄。

顯示輸出

exec.Cmd對象有兩個字段StdoutStderr,類型皆為io.Writer。我們可以將任意實現(xiàn)了io.Writer接口的類型實例賦給這兩個字段,繼而實現(xiàn)標準輸出和標準錯誤的重定向。io.Writer接口在 Go 標準庫和第三方庫中隨處可見,例如*os.File*bytes.Buffer、net.Conn。所以我們可以將命令的輸出重定向到文件、內(nèi)存緩存甚至發(fā)送到網(wǎng)絡(luò)中。

顯示到標準輸出

exec.Cmd對象的StdoutStderr這兩個字段都設(shè)置為os.Stdout,那么輸出內(nèi)容都將顯示到標準輸出:

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運行,得到如下結(jié)果:

輸出了中文,檢查一下環(huán)境變量LANG的值,果然是zh_CN.UTF-8。如果想輸出英文,可以將環(huán)境變量LANG設(shè)置為en_US.UTF-8

$ echo $LANG
zh_CN.UTF-8
$ LANG=en_US.UTF-8 go run main.go

得到輸出:

輸出到文件

打開或創(chuàng)建文件,然后將文件句柄賦給exec.Cmd對象的StdoutStderr這兩個字段即可實現(xiàn)輸出到文件的功能。

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標志讓操作系統(tǒng)在文件不存在時自動創(chuàng)建一個,返回該文件對象*os.File。*os.File實現(xiàn)了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

發(fā)送到網(wǎng)絡(luò)

現(xiàn)在我們來編寫一個日歷服務(wù),接收年、月信息,返回該月的日歷。

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參數(shù)都需要做合法性校驗。exec.Command函數(shù)接收一個字符串類型的可變參數(shù)作為命令的參數(shù):

func Command(name string, arg ...string) *Cmd

運行程序,使用瀏覽器請求localhost:8080/cal?year=2021&month=2得到:

保存到內(nèi)存對象中

*bytes.Buffer同樣也實現(xiàn)了io.Writer接口,故如果我們創(chuàng)建一個*bytes.Buffer對象,并將其賦給exec.CmdStdoutStderr這兩個字段,那么命令執(zhí)行之后,該*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

運行命令,然后得到輸出的字符串或字節(jié)切片這種模式是如此的普遍,并且使用便利,os/exec包提供了一個便捷方法:CombinedOutput。

輸出到多個目的地

有時,我們希望能輸出到文件和網(wǎng)絡(luò),同時保存到內(nèi)存對象。使用go提供的io.MultiWriter可以很容易實現(xiàn)這個需求。io.MultiWriter很方便地將多個io.Writer轉(zhuǎn)為一個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())
}

調(diào)用io.MultiWriter將多個io.Writer整合成一個io.Writer,然后將cmd對象的StdoutStderr都賦值為這個io.Writer。這樣,命令運行時產(chǎn)出的輸出會分別送往http.ResponseWriter、*os.File以及*bytes.Buffer

運行命令,獲取輸出

前面提到,我們常常需要運行命令,返回輸出。exec.Cmd對象提供了一個便捷方法:CombinedOutput()。該方法運行命令,將輸出內(nèi)容以一個字節(jié)切片返回便于后續(xù)處理。所以,上面獲取輸出的程序可以簡化為:

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()方法的實現(xiàn)很簡單,先將標準輸出和標準錯誤重定向到*bytes.Buffer對象,然后運行程序,最后返回該對象中的字節(jié)切片:

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方法前幾行判斷表明,StdoutStderr必須是未設(shè)置狀態(tài)。這其實很好理解,一般情況下,如果已經(jīng)打算使用CombinedOutput方法獲取輸出內(nèi)容,不會再自找麻煩地再去設(shè)置StdoutStderr字段了。

CombinedOutput類似的還有Output方法,區(qū)別是Output只會返回運行命令產(chǎn)出的標準輸出內(nèi)容。

分別獲取標準輸出和標準錯誤

創(chuàng)建兩個*bytes.Buffer對象,分別賦給exec.Cmd對象的StdoutStderr這兩個字段,然后運行命令即可分別獲取標準輸出和標準錯誤。

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)
  }
}

如果不帶參數(shù)運行cat命令,則進入交互模式,cat按行讀取輸入,并且原樣發(fā)送到輸出。

再來看一個復(fù)雜點的例子。Go標準庫中compress/bzip2包只提供解壓方法,并沒有壓縮方法。我們可以利用Linux命令bzip2實現(xiàn)壓縮。bzip2從標準輸入中讀取數(shù)據(jù),將其壓縮,并發(fā)送到標準輸出。

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
}

參數(shù)-c表示壓縮,-9表示壓縮等級,9為最高。為了驗證函數(shù)的正確性,寫個簡單的程序,先壓縮"hello world"字符串,然后解壓,看看是否能得到原來的字符串:

func main() {
  data := []byte("hello world")
  compressed, _ := bzipCompress(data)
  r := bzip2.NewReader(bytes.NewBuffer(compressed))
  decompressed, _ := ioutil.ReadAll(r)
  fmt.Println(string(decompressed))
}

運行程序,輸出"hello world"。

環(huán)境變量

環(huán)境變量可以在一定程度上微調(diào)程序的行為,當然這需要程序的支持。例如,設(shè)置ENV=production會抑制調(diào)試日志的輸出。每個環(huán)境變量都是一個鍵值對。exec.Cmd對象中有一個類型為[]string的字段Env。我們可以通過修改它來達到控制命令運行時的環(huán)境變量的目的。

package main

import (
  "fmt"
  "log"
  "os"
  "os/exec"
)

func main() {
  cmd := exec.Command("bash", "-c", "./test.sh")

  nameEnv := "NAME=darjun"
  ageEnv := "AGE=18"

  newEnv := append(os.Environ(), nameEnv, ageEnv)
  cmd.Env = newEnv

  out, err := cmd.CombinedOutput()
  if err != nil {
    log.Fatalf("cmd.Run() failed: %v\n", err)
  }

  fmt.Println(string(out))
}

上面代碼獲取系統(tǒng)的環(huán)境變量,然后又添加了兩個環(huán)境變量NAMEAGE。最后使用bash運行腳本test.sh

#!/bin/bash

echo $NAME
echo $AGE
echo $GOPATH

程序運行結(jié)果:

$ go run main.go 
darjun
18
D:\workspace\code\go

檢查命令是否存在

一般在運行命令之前,我們通過希望能檢查要運行的命令是否存在,如果存在則直接運行,否則提示用戶安裝此命令。os/exec包提供了函數(shù)LookPath可以獲取命令所在目錄,如果命令不存在,則返回一個error。

func main() {
  path, err := exec.LookPath("ls")
  if err != nil {
    fmt.Printf("no cmd ls: %v\n", err)
  } else {
    fmt.Printf("find ls in path:%s\n", path)
  }

  path, err = exec.LookPath("not-exist")
  if err != nil {
    fmt.Printf("no cmd not-exist: %v\n", err)
  } else {
    fmt.Printf("find not-exist in path:%s\n", path)
  }
}

運行:

$ go run main.go 
find ls in path:C:\Program Files\Git\usr\bin\ls.exe
no cmd not-exist: exec: "not-exist": executable file not found in %PATH%

封裝

執(zhí)行外部命令的流程比較固定:

  • 調(diào)用exec.Command()創(chuàng)建命令對象;
  • 調(diào)用Cmd.Run()執(zhí)行命令

如果要獲取輸出,需要調(diào)用CombinedOutput/Output之類的方法,或者手動創(chuàng)建bytes.Buffer對象并賦值給exec.CmdStdoutStderr字段。為了使用方便,我編寫了一個包goexec。

接口如下:

// 執(zhí)行命令,丟棄標準輸出和標準錯誤
func RunCommand(cmd string, arg []string, opts ...Option) error
// 執(zhí)行命令,以[]byte類型返回輸出
func CombinedOutput(cmd string, arg []string, opts ...Option) ([]byte, error)
// 執(zhí)行命令,以string類型返回輸出
func CombinedOutputString(cmd string, arg []string, opts ...Option) (string, error)
// 執(zhí)行命令,以[]byte類型返回標準輸出
func Output(cmd string, arg []string, opts ...Option) ([]byte, error)
// 執(zhí)行命令,以string類型返回標準輸出
func OutputString(cmd string, arg []string, opts ...Option) (string, error)
// 執(zhí)行命令,以[]byte類型分別返回標準輸出和標準錯誤
func SeparateOutput(cmd string, arg []string, opts ...Option) ([]byte, []byte, error)
// 執(zhí)行命令,以string類型分別返回標準輸出和標準錯誤
func SeparateOutputString(cmd string, arg []string, opts ...Option) (string, string, error)

相較于直接使用os/exec包,我傾向于一次函數(shù)調(diào)用就能獲得結(jié)果。對輸入、設(shè)置環(huán)境變量這些功能,我通過Option模式來提供支持。

type Option func(*exec.Cmd)

func WithStdin(stdin io.Reader) Option {
  return func(c *exec.Cmd) {
    c.Stdin = stdin
  }
}

func Without(stdout io.Writer) Option {
  return func(c *exec.Cmd) {
    c.Stdout = stdout
  }
}

func WithStderr(stderr io.Writer) Option {
  return func(c *exec.Cmd) {
    c.Stderr = stderr
  }
}

func WithOutWriter(out io.Writer) Option {
  return func(c *exec.Cmd) {
    c.Stdout = out
    c.Stderr = out
  }
}

func WithEnv(key, value string) Option {
  return func(c *exec.Cmd) {
    c.Env = append(os.Environ(), fmt.Sprintf("%s=%s", key, value))
  }
}

func applyOptions(cmd *exec.Cmd, opts []Option) {
  for _, opt := range opts {
    opt(cmd)
  }
}

使用非常簡單:

func main() {
  fmt.Println(goexec.CombinedOutputString("cal", nil, goexec.WithEnv("LANG", "en_US.UTF-8")))
}

有一點我不太滿意,為了使用Option模式,本來可以用可變參數(shù)來傳遞命令參數(shù),現(xiàn)在只能用切片了,即使不需要指定參數(shù),也必須要傳入一個nil。暫時還沒有想到比較優(yōu)雅的解決方法。

總結(jié)

本文介紹了使用os/exec這個標準庫調(diào)用外部命令的各種姿勢。同時為了便于使用,我編寫了一個goexec包封裝對os/exec的調(diào)用。這個包目前for我自己使用是沒有問題的,大家有其他需求可以提issue或者自己魔改。

到此這篇關(guān)于Go語言中調(diào)用外部命令的方法總結(jié)的文章就介紹到這了,更多相關(guān)Go語言調(diào)用外部命令內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • go?defer延遲調(diào)用的使用場景示例詳解

    go?defer延遲調(diào)用的使用場景示例詳解

    這篇文章主要為大家介紹了go?defer延遲調(diào)用的使用場景示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-05-05
  • Go語言學習教程之結(jié)構(gòu)體的示例詳解

    Go語言學習教程之結(jié)構(gòu)體的示例詳解

    結(jié)構(gòu)體是一個序列,包含一些被命名的元素,這些被命名的元素稱為字段(field),每個字段有一個名字和一個類型。本文通過一些示例帶大家深入了解Go語言中結(jié)構(gòu)體的使用,需要的可以參考一下
    2022-09-09
  • Golang如何自定義logrus日志保存為日志文件

    Golang如何自定義logrus日志保存為日志文件

    這篇文章主要給大家介紹了關(guān)于Golang如何自定義logrus日志保存為日志文件的相關(guān)資料,logrus是目前Github上star數(shù)量最多的日志庫,logrus功能強大,性能高效,而且具有高度靈活性,提供了自定義插件的功能,很多開源項目都是用了logrus來記錄其日志,需要的朋友可以參考下
    2024-02-02
  • Golang搭建HTTP服務(wù)器

    Golang搭建HTTP服務(wù)器

    Golang是一種非常流行的編程語言,它的開發(fā)速度快,代碼運行效率高等特點非常適合做Web應(yīng)用的開發(fā)。本文將介紹如何使用Golang搭建HTTP服務(wù)器,需要的朋友可以參考閱讀
    2023-04-04
  • golang qq郵件發(fā)送驗證碼功能

    golang qq郵件發(fā)送驗證碼功能

    驗證碼在多個場景下發(fā)揮著重要作用,如注冊/登錄、短信接口保護、提交/投票、密碼找回和支付驗證等,以保障賬號安全和防止惡意操作,此外,文章還介紹了使用golang通過qq郵件發(fā)送驗證碼的實現(xiàn)過程,包括獲取授權(quán)碼、下載依賴包和編寫代碼,感興趣的朋友跟隨小編一起看看吧
    2024-09-09
  • Golang?channel底層實現(xiàn)過程解析(深度好文)

    Golang?channel底層實現(xiàn)過程解析(深度好文)

    Go語言為了方便使用者,提供了簡單、安全的協(xié)程數(shù)據(jù)同步和通信機制,這篇文章主要介紹了Golang?channel底層是如何實現(xiàn)的,需要的朋友可以參考下
    2024-07-07
  • Go事務(wù)中止時是否真的結(jié)束事務(wù)解析

    Go事務(wù)中止時是否真的結(jié)束事務(wù)解析

    這篇文章主要為大家介紹了Go事務(wù)中止時是否真的結(jié)束事務(wù)實例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-04-04
  • Go實現(xiàn)set類型的示例代碼

    Go實現(xiàn)set類型的示例代碼

    本文主要介紹了Go實現(xiàn)set類型的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-01-01
  • golang高性能的http請求 fasthttp詳解

    golang高性能的http請求 fasthttp詳解

    fasthttp 是 Go 的快速 HTTP 實現(xiàn),當前在 1M 并發(fā)的生產(chǎn)環(huán)境使用非常成功,可以從單個服務(wù)器進行 100K qps 的持續(xù)連接,總而言之,fasthttp 比 net/http 快 10 倍,下面通過本文給大家介紹golang fasthttp http請求的相關(guān)知識,一起看看吧
    2021-09-09
  • Go語言的方法接受者類型用值類型還是指針類型?

    Go語言的方法接受者類型用值類型還是指針類型?

    這篇文章主要介紹了Go語言的方法接受者類型用值類型還是指針類型?本文還同時講解了關(guān)于接受者的命名方式,需要的朋友可以參考下
    2014-10-10

最新評論

壶关县| 景宁| 阳原县| 固原市| 长乐市| 织金县| 长丰县| 石泉县| 芮城县| 望城县| 乃东县| 珲春市| 柳州市| 吉隆县| 奉化市| 格尔木市| 微博| 海门市| 昌乐县| 孟津县| 千阳县| 渭南市| 普定县| 古浪县| 南昌县| 祥云县| 桐梓县| 小金县| 万盛区| 衡阳市| 郧西县| 如皋市| 门源| 三江| 项城市| 遵化市| 龙山县| 固阳县| 宜州市| 桃园县| 句容市|