Go語言中調(diào)用外部命令的方法總結(jié)
引子
在工作中,我時(shí)不時(shí)地會(huì)需要在Go中調(diào)用外部命令。前段時(shí)間我做了一個(gè)工具,在釘釘群中添加了一個(gè)機(jī)器人,@這個(gè)機(jī)器人可以讓它執(zhí)行一些寫好的腳本程序完成指定的任務(wù)。機(jī)器人倒是不難,照著釘釘開發(fā)者文檔添加好機(jī)器人,然后@這個(gè)機(jī)器人就會(huì)向一個(gè)你指定的服務(wù)器發(fā)送一個(gè)POST請(qǐng)求,請(qǐng)求中會(huì)附帶文本消息。所以我要做的就是搭一個(gè)Web服務(wù)器,可以用go原生的net/http包,也可以用gin/fasthttp/fiber這些Web框架。收到請(qǐng)求之后,檢查附帶文本中的關(guān)鍵字去調(diào)用對(duì)應(yīng)的程序,然后返回結(jié)果。
go標(biāo)準(zhǔn)庫(kù)中的os/exec包對(duì)調(diào)用外部程序提供了支持,本文詳細(xì)介紹os/exec的使用姿勢(shì)。
運(yùn)行命令
Linux中有個(gè)cal
命令,它可以顯示指定年、月的日歷,如果不指定年、月,默認(rèn)為當(dāng)前時(shí)間對(duì)應(yīng)的年月。如果使用的是Windows,推薦安裝msys2,這個(gè)軟件包含了絕大多數(shù)的Linux常用命令。
那么,在Go代碼中怎么調(diào)用這個(gè)命令呢?其實(shí)也很簡(jiǎn)單:
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)建一個(gè)命令對(duì)象exec.Cmd
。接著調(diào)用該命令對(duì)象的Run()
方法運(yùn)行它。
如果你實(shí)際運(yùn)行了,你會(huì)發(fā)現(xiàn)什么也沒有發(fā)生,哈哈。事實(shí)上,使用os/exec執(zhí)行命令,標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤默認(rèn)會(huì)被丟棄。
顯示輸出
exec.Cmd
對(duì)象有兩個(gè)字段Stdout
和Stderr
,類型皆為io.Writer
。我們可以將任意實(shí)現(xiàn)了io.Writer
接口的類型實(shí)例賦給這兩個(gè)字段,繼而實(shí)現(xiàn)標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤的重定向。io.Writer
接口在 Go 標(biāo)準(zhǔn)庫(kù)和第三方庫(kù)中隨處可見,例如*os.File
、*bytes.Buffer
、net.Conn
。所以我們可以將命令的輸出重定向到文件、內(nèi)存緩存甚至發(fā)送到網(wǎng)絡(luò)中。
顯示到標(biāo)準(zhǔn)輸出
將exec.Cmd
對(duì)象的Stdout
和Stderr
這兩個(gè)字段都設(shè)置為os.Stdout
,那么輸出內(nèi)容都將顯示到標(biāo)準(zhǔn)輸出:
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) } }
運(yùn)行程序。我在git bash運(yùn)行,得到如下結(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
對(duì)象的Stdout
和Stderr
這兩個(gè)字段即可實(shí)現(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
打開一個(gè)文件,指定os.O_CREATE
標(biāo)志讓操作系統(tǒng)在文件不存在時(shí)自動(dòng)創(chuàng)建一個(gè),返回該文件對(duì)象*os.File
。*os.File
實(shí)現(xiàn)了io.Writer
接口。
運(yùn)行程序:
$ 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)在我們來編寫一個(gè)日歷服務(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) }
這里為了簡(jiǎn)單,錯(cuò)誤處理都省略了。正常情況下,year和month參數(shù)都需要做合法性校驗(yàn)。exec.Command
函數(shù)接收一個(gè)字符串類型的可變參數(shù)作為命令的參數(shù):
func Command(name string, arg ...string) *Cmd
運(yùn)行程序,使用瀏覽器請(qǐng)求localhost:8080/cal?year=2021&month=2
得到:
保存到內(nèi)存對(duì)象中
*bytes.Buffer
同樣也實(shí)現(xiàn)了io.Writer
接口,故如果我們創(chuàng)建一個(gè)*bytes.Buffer
對(duì)象,并將其賦給exec.Cmd
的Stdout
和Stderr
這兩個(gè)字段,那么命令執(zhí)行之后,該*bytes.Buffer
對(duì)象中保存的就是命令的輸出。
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()) }
運(yùn)行:
$ 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
運(yùn)行命令,然后得到輸出的字符串或字節(jié)切片這種模式是如此的普遍,并且使用便利,os/exec
包提供了一個(gè)便捷方法:CombinedOutput
。
輸出到多個(gè)目的地
有時(shí),我們希望能輸出到文件和網(wǎng)絡(luò),同時(shí)保存到內(nèi)存對(duì)象。使用go提供的io.MultiWriter
可以很容易實(shí)現(xiàn)這個(gè)需求。io.MultiWriter
很方便地將多個(gè)io.Writer
轉(zhuǎn)為一個(gè)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
將多個(gè)io.Writer
整合成一個(gè)io.Writer
,然后將cmd對(duì)象的Stdout
和Stderr
都賦值為這個(gè)io.Writer
。這樣,命令運(yùn)行時(shí)產(chǎn)出的輸出會(huì)分別送往http.ResponseWriter
、*os.File
以及*bytes.Buffer
。
運(yùn)行命令,獲取輸出
前面提到,我們常常需要運(yùn)行命令,返回輸出。exec.Cmd
對(duì)象提供了一個(gè)便捷方法:CombinedOutput()
。該方法運(yùn)行命令,將輸出內(nèi)容以一個(gè)字節(jié)切片返回便于后續(xù)處理。所以,上面獲取輸出的程序可以簡(jiǎn)化為:
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()
方法的實(shí)現(xiàn)很簡(jiǎn)單,先將標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤重定向到*bytes.Buffer
對(duì)象,然后運(yùn)行程序,最后返回該對(duì)象中的字節(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
方法前幾行判斷表明,Stdout
和Stderr
必須是未設(shè)置狀態(tài)。這其實(shí)很好理解,一般情況下,如果已經(jīng)打算使用CombinedOutput
方法獲取輸出內(nèi)容,不會(huì)再自找麻煩地再去設(shè)置Stdout
和Stderr
字段了。
與CombinedOutput
類似的還有Output
方法,區(qū)別是Output
只會(huì)返回運(yùn)行命令產(chǎn)出的標(biāo)準(zhǔn)輸出內(nèi)容。
分別獲取標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤
創(chuàng)建兩個(gè)*bytes.Buffer
對(duì)象,分別賦給exec.Cmd
對(duì)象的Stdout
和Stderr
這兩個(gè)字段,然后運(yùn)行命令即可分別獲取標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤。
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()) }
標(biāo)準(zhǔn)輸入
exec.Cmd
對(duì)象有一個(gè)類型為io.Reader
的字段Stdin
。命令運(yùn)行時(shí)會(huì)從這個(gè)io.Reader
讀取輸入。先來看一個(gè)最簡(jiǎn)單的例子:
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ù)運(yùn)行cat
命令,則進(jìn)入交互模式,cat
按行讀取輸入,并且原樣發(fā)送到輸出。
再來看一個(gè)復(fù)雜點(diǎn)的例子。Go標(biāo)準(zhǔn)庫(kù)中compress/bzip2
包只提供解壓方法,并沒有壓縮方法。我們可以利用Linux命令bzip2
實(shí)現(xiàn)壓縮。bzip2
從標(biāo)準(zhǔn)輸入中讀取數(shù)據(jù),將其壓縮,并發(fā)送到標(biāo)準(zhǔn)輸出。
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
表示壓縮等級(jí),9為最高。為了驗(yàn)證函數(shù)的正確性,寫個(gè)簡(jiǎn)單的程序,先壓縮"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)) }
運(yùn)行程序,輸出"hello world"。
環(huán)境變量
環(huán)境變量可以在一定程度上微調(diào)程序的行為,當(dāng)然這需要程序的支持。例如,設(shè)置ENV=production
會(huì)抑制調(diào)試日志的輸出。每個(gè)環(huán)境變量都是一個(gè)鍵值對(duì)。exec.Cmd
對(duì)象中有一個(gè)類型為[]string
的字段Env
。我們可以通過修改它來達(dá)到控制命令運(yùn)行時(shí)的環(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)境變量,然后又添加了兩個(gè)環(huán)境變量NAME
和AGE
。最后使用bash
運(yùn)行腳本test.sh
:
#!/bin/bash echo $NAME echo $AGE echo $GOPATH
程序運(yùn)行結(jié)果:
$ go run main.go
darjun
18
D:\workspace\code\go
檢查命令是否存在
一般在運(yùn)行命令之前,我們通過希望能檢查要運(yùn)行的命令是否存在,如果存在則直接運(yùn)行,否則提示用戶安裝此命令。os/exec
包提供了函數(shù)LookPath
可以獲取命令所在目錄,如果命令不存在,則返回一個(gè)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) } }
運(yùn)行:
$ 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)建命令對(duì)象; - 調(diào)用
Cmd.Run()
執(zhí)行命令
如果要獲取輸出,需要調(diào)用CombinedOutput/Output
之類的方法,或者手動(dòng)創(chuàng)建bytes.Buffer
對(duì)象并賦值給exec.Cmd
的Stdout
和Stderr
字段。為了使用方便,我編寫了一個(gè)包goexec
。
接口如下:
// 執(zhí)行命令,丟棄標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤 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類型返回標(biāo)準(zhǔn)輸出 func Output(cmd string, arg []string, opts ...Option) ([]byte, error) // 執(zhí)行命令,以string類型返回標(biāo)準(zhǔn)輸出 func OutputString(cmd string, arg []string, opts ...Option) (string, error) // 執(zhí)行命令,以[]byte類型分別返回標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤 func SeparateOutput(cmd string, arg []string, opts ...Option) ([]byte, []byte, error) // 執(zhí)行命令,以string類型分別返回標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤 func SeparateOutputString(cmd string, arg []string, opts ...Option) (string, string, error)
相較于直接使用os/exec
包,我傾向于一次函數(shù)調(diào)用就能獲得結(jié)果。對(duì)輸入、設(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) } }
使用非常簡(jiǎn)單:
func main() { fmt.Println(goexec.CombinedOutputString("cal", nil, goexec.WithEnv("LANG", "en_US.UTF-8"))) }
有一點(diǎn)我不太滿意,為了使用Option模式,本來可以用可變參數(shù)來傳遞命令參數(shù),現(xiàn)在只能用切片了,即使不需要指定參數(shù),也必須要傳入一個(gè)nil
。暫時(shí)還沒有想到比較優(yōu)雅的解決方法。
總結(jié)
本文介紹了使用os/exec
這個(gè)標(biāo)準(zhǔn)庫(kù)調(diào)用外部命令的各種姿勢(shì)。同時(shí)為了便于使用,我編寫了一個(gè)goexec包封裝對(duì)os/exec
的調(diào)用。這個(gè)包目前for我自己使用是沒有問題的,大家有其他需求可以提issue或者自己魔改。
到此這篇關(guān)于Go語言中調(diào)用外部命令的方法總結(jié)的文章就介紹到這了,更多相關(guān)Go語言調(diào)用外部命令內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用go實(shí)現(xiàn)刪除sql里面的注釋和字符串功能(demo)
這篇文章主要介紹了使用go實(shí)現(xiàn)刪除sql里面的注釋和字符串功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11Golang使用crypto/ed25519實(shí)現(xiàn)數(shù)字簽名和驗(yàn)證
本文將深入探討如何在?Golang?中使用?crypto/ed25519?進(jìn)行數(shù)字簽名和驗(yàn)證,我們將從基本原理開始,逐步引導(dǎo)讀者了解生成密鑰對(duì)、進(jìn)行數(shù)字簽名,以及驗(yàn)證簽名的具體過程,希望對(duì)大家有所幫助2024-02-02淺析Go設(shè)計(jì)模式之Facade(外觀)模式
本文將介紹外觀模式的概念、結(jié)構(gòu)和工作原理,并提供一些在Go中實(shí)現(xiàn)外觀模式的示例代碼,通過使用外觀模式,可以降低代碼的耦合度,提高代碼的可維護(hù)性和可讀性,需要的朋友可以參考下2023-05-05Go實(shí)現(xiàn)簡(jiǎn)易R(shí)PC框架的方法步驟
本文旨在講述 RPC 框架設(shè)計(jì)中的幾個(gè)核心問題及其解決方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-03-03GO將mysql?中?decimal?數(shù)據(jù)類型映射到?protobuf的操作方法
這篇文章主要介紹了go如何優(yōu)雅地將?mysql?中?decimal?數(shù)據(jù)類型映射到?protobuf,本文主要展示一下在 protobuf中 float與double的一個(gè)區(qū)別,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09使用Lumberjack+zap進(jìn)行日志切割歸檔操作
這篇文章主要介紹了使用Lumberjack+zap進(jìn)行日志切割歸檔操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12解析GOROOT、GOPATH、Go-Modules-三者的關(guān)系
這篇文章主要介紹了解析GOROOT、GOPATH、Go-Modules-三者的關(guān)系,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10