新聞中心
近期在Twitter上看到一個(gè)名為“Command Line Interface Guidelines”的站點(diǎn)[1],這個(gè)站點(diǎn)匯聚了幫助大家編寫(xiě)出更好命令行程序的哲學(xué)與指南。這份指南基于傳統(tǒng)的Unix編程原則[2],又結(jié)合現(xiàn)代的情況進(jìn)行了“與時(shí)俱進(jìn)”的更新。之前我還真未就如何編寫(xiě)命令行交互程序做系統(tǒng)的梳理,在這篇文章中,我們就來(lái)結(jié)合clig這份指南[3],(可能不會(huì)全面覆蓋)整理出一份使用Go語(yǔ)言編寫(xiě)CLI程序的指南,供大家參考。

成都創(chuàng)新互聯(lián)長(zhǎng)期為上千余家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對(duì)不同對(duì)象提供差異化的產(chǎn)品和服務(wù);打造開(kāi)放共贏平臺(tái),與合作伙伴共同營(yíng)造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為麻栗坡企業(yè)提供專業(yè)的成都做網(wǎng)站、成都網(wǎng)站建設(shè)、成都外貿(mào)網(wǎng)站建設(shè),麻栗坡網(wǎng)站改版等技術(shù)服務(wù)。擁有十余年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開(kāi)發(fā)。
一. 命令行程序簡(jiǎn)介
命令行接口(Command Line Interface, 簡(jiǎn)稱CLI)程序是一種允許用戶使用文本命令和參數(shù)與計(jì)算機(jī)系統(tǒng)互動(dòng)的軟件。開(kāi)發(fā)人員編寫(xiě)CLI程序通常用在自動(dòng)化腳本、數(shù)據(jù)處理、系統(tǒng)管理和其他需要低級(jí)控制和靈活性的任務(wù)上。命令行程序也是Linux/Unix管理員以及后端開(kāi)發(fā)人員的最愛(ài)。
2022年Q2 Go官方用戶調(diào)查結(jié)果[4]顯示(如下圖):在使用Go開(kāi)發(fā)的程序類別上,CLI類程序排行第二,得票率60%。
之所以這樣,得益于Go語(yǔ)言為CLI開(kāi)發(fā)提供的諸多便利,比如:
- Go語(yǔ)法簡(jiǎn)單而富有表現(xiàn)力;
- Go擁有一個(gè)強(qiáng)大的標(biāo)準(zhǔn)庫(kù),并內(nèi)置的并發(fā)支持;
- Go擁有幾乎最好的跨平臺(tái)兼容性和快速的編譯速度;
- Go還有一個(gè)豐富的第三方軟件包和工具的生態(tài)系統(tǒng)。
這些都讓開(kāi)發(fā)者使用Go創(chuàng)建強(qiáng)大和用戶友好的CLI程序變得容易。
容易歸容易,但要用Go編寫(xiě)出優(yōu)秀的CLI程序,我們還需要遵循一些原則,獲得一些關(guān)于Go CLI程序開(kāi)發(fā)的最佳實(shí)踐和慣例。這些原則和慣例涉及交互界面設(shè)計(jì)、錯(cuò)誤處理、文檔、測(cè)試和發(fā)布等主題。此外,借助于一些流行的Go CLI程序開(kāi)發(fā)庫(kù)和框架,比如:cobra[5]、Kingpin[6]和Goreleaser[7]等,我們可以又好又快地完成CLI程序的開(kāi)發(fā)。在本文結(jié)束時(shí),你將學(xué)會(huì)如何創(chuàng)建一個(gè)易于使用、可靠和可維護(hù)的Go CLI程序,你還將獲得一些關(guān)于CLI開(kāi)發(fā)的最佳實(shí)踐和慣例的見(jiàn)解。
二. 建立Go開(kāi)發(fā)環(huán)境
如果你讀過(guò)《十分鐘入門Go語(yǔ)言》[8]或訂閱學(xué)習(xí)過(guò)我的極客時(shí)間《Go語(yǔ)言第一課》專欄[9],你大可忽略這一節(jié)的內(nèi)容。
在我們開(kāi)始編寫(xiě)Go CLI程序之前,我們需要確保我們的系統(tǒng)中已經(jīng)安裝和配置了必要的Go工具和依賴。在本節(jié)中,我們將向你展示如何安裝Go和設(shè)置你的工作空間,如何使用go mod進(jìn)行依賴管理[10],以及如何使用go build和go install來(lái)編譯和安裝你的程序。
1. 安裝Go
要在你的系統(tǒng)上安裝Go,你可以遵循你所用操作系統(tǒng)的官方安裝說(shuō)明。你也可以使用軟件包管理器,如homebrew[11](用于macOS)、chocolatey(用于Windows)或snap/apt(用于Linux)來(lái)更容易地安裝Go。
一旦你安裝了Go,你可以通過(guò)在終端運(yùn)行以下命令來(lái)驗(yàn)證它是否可以正常工作。
$go version
如果安裝成功,go version這個(gè)命令應(yīng)該會(huì)打印出你所安裝的Go的版本。比如說(shuō):
go version go1.20 darwin/amd64
2. 設(shè)置你的工作區(qū)(workspace)
Go以前有一個(gè)慣例,即在工作區(qū)目錄中(組織你的代碼和依賴關(guān)系。默認(rèn)工作空間目錄位于HOME/go,但你可以通過(guò)設(shè)置GOPATH環(huán)境變量來(lái)改變它的路徑。工作區(qū)目錄包含三個(gè)子目錄:src、pkg和bin。src目錄包含了你的源代碼文件和目錄。pkg目錄包含被你的代碼導(dǎo)入的已編譯好的包。bin目錄包含由你的代碼生成的可執(zhí)行二進(jìn)制文件。
Go 1.11引入Go module[12]后,這種在下組織代碼和尋找依賴關(guān)系的要求被徹底取消。在這篇文章中,我依舊按照我的習(xí)慣在HOME/go/src下放置我的代碼示例。
為了給我們的CLI程序創(chuàng)建一個(gè)新的項(xiàng)目目錄,我們可以在終端運(yùn)行以下命令:
$mkdir -p $HOME/go/src/github.com/your-username/your-li-program
$cd $HOME/go/src/github.com/your-username/your-cli-program
注意,我們的項(xiàng)目目錄名使用的是github的URL格式。這在Go項(xiàng)目中是一種常見(jiàn)的做法,因?yàn)樗沟檬褂胓o get導(dǎo)入和管理依賴關(guān)系更加容易。go module成為構(gòu)建標(biāo)準(zhǔn)后,這種對(duì)項(xiàng)目目錄名的要求已經(jīng)取消,但很多Gopher依舊保留了這種作法。
3. 使用go mod進(jìn)行依賴管理
1.11版本后Go推薦開(kāi)發(fā)者使用module來(lái)管理包的依賴關(guān)系。一個(gè)module是共享一個(gè)共同版本號(hào)和導(dǎo)入路徑前綴的相關(guān)包的集合。一個(gè)module是由一個(gè)叫做go.mod的文件定義的,它指定了模塊的名稱、版本和依賴關(guān)系。
為了給我們的CLI程序創(chuàng)建一個(gè)新的module,我們可以在我們的項(xiàng)目目錄下運(yùn)行以下命令。
$go mod init github.com/your-username/your-cli-program
這將創(chuàng)建一個(gè)名為go.mod的文件,內(nèi)容如下。
module github.com/your-username/your-cli-program
go 1.20
第一行指定了我們的module名稱,這與我們的項(xiàng)目目錄名稱相匹配。第二行指定了構(gòu)建我們的module所需的Go的最低版本。
為了給我們的模塊添加依賴項(xiàng),我們可以使用go get命令,加上我們想使用的軟件包的導(dǎo)入路徑和可選的版本標(biāo)簽。例如,如果我們想使用cobra[13]作為我們的CLI框架,我們可以運(yùn)行如下命令:
$go get github.com/spf13/cobra@v1.3.0
go get將從github下載cobra,并在我們的go.mod文件中把它作為一個(gè)依賴項(xiàng)添加進(jìn)去。它還將創(chuàng)建或更新一個(gè)名為go.sum的文件,記錄所有下載的module的校驗(yàn)和,以供后續(xù)驗(yàn)證使用。
我們還可以使用其他命令,如go list、go mod tidy、go mod graph等,以更方便地檢查和管理我們的依賴關(guān)系。
4. 使用go build和go install來(lái)編譯和安裝你的程序
Go有兩個(gè)命令允許你編譯和安裝你的程序:go build和go install。這兩個(gè)命令都以一個(gè)或多個(gè)包名或?qū)肼窂阶鳛閰?shù),并從中產(chǎn)生可執(zhí)行的二進(jìn)制文件。
它們之間的主要區(qū)別在于它們將生成的二進(jìn)制文件存儲(chǔ)在哪里。
- go build將它們存儲(chǔ)在當(dāng)前工作目錄中。
- go install將它們存儲(chǔ)在或GOBIN(如果設(shè)置了)。
例如,如果我們想把CLI程序的main包(應(yīng)該位于github.com/your-username/your-cli-program/cmd/your-cli-program)編譯成一個(gè)可執(zhí)行的二進(jìn)制文件,稱為your-cli-program,我們可以運(yùn)行下面命令:
$go build github.com/your-username/your-cli-program/cmd/your-cli-program
或
$go install github.com/your-username/your-cli-program/cmd/your-cli-program@latest
三. 設(shè)計(jì)用戶接口(interface)
要編寫(xiě)出一個(gè)好的CLI程序,最重要的環(huán)節(jié)之一是**設(shè)計(jì)一個(gè)用戶友好的接口[14]。好的命令行用戶接口應(yīng)該是一致的、直觀的和富有表現(xiàn)力的**。在本節(jié)中,我將說(shuō)明如何為命令行程序命名和選擇命令結(jié)構(gòu)(command structure),如何使用標(biāo)志(flag)、參數(shù)(argument)、子命令(subcommand)和選項(xiàng)(option)作為輸入?yún)?shù),如何使用cobra或Kingpin等來(lái)解析和驗(yàn)證用戶輸入,以及如何遵循POSIX慣例和GNU擴(kuò)展的CLI語(yǔ)法。
1. 命令行程序命名和命令結(jié)構(gòu)選擇
你的CLI程序的名字應(yīng)該是**簡(jiǎn)短、易記、描述性的和易輸入的[15]**。它應(yīng)該避免與目標(biāo)平臺(tái)中現(xiàn)有的命令或關(guān)鍵字發(fā)生沖突。例如,如果你正在編寫(xiě)一個(gè)在不同格式之間轉(zhuǎn)換圖像的程序,你可以把它命名為imgconv、imago、picto等,但不能叫image、convert或format。
你的CLI程序的命令結(jié)構(gòu)應(yīng)該反映你想提供給用戶的主要功能特性。你可以選擇使用下面命令結(jié)構(gòu)模式中的一種:
- 一個(gè)帶有多個(gè)標(biāo)志(flag)和參數(shù)(argument)的單一命令(例如:curl、tar、grep等)
- 帶有多個(gè)子命令(subcommand)的單一命令(例如:git、docker、kubectl等)
- 具有共同前綴的多個(gè)命令(例如:aws s3、gcloud compute、az vm等)
命令結(jié)構(gòu)模式的選擇取決于你的程序的復(fù)雜性和使用范圍,一般來(lái)說(shuō):
- 如果你的程序只有一個(gè)主要功能或操作模式(operation mode),你可以使用帶有多個(gè)標(biāo)志和參數(shù)的單一命令。
- 如果你的程序有多個(gè)相關(guān)但又不同的功能或操作模式,你可以使用一個(gè)帶有多個(gè)子命令的單一命令。
- 如果你的程序有多個(gè)不相關(guān)或獨(dú)立的功能或操作模式,你可以使用具有共同前綴的多個(gè)命令。
例如,如果你正在編寫(xiě)一個(gè)對(duì)文件進(jìn)行各種操作的程序(如復(fù)制、移動(dòng)、刪除),你可以任選下面命令結(jié)構(gòu)模式中的一種:
- 帶有多個(gè)標(biāo)志和參數(shù)的單一命令(例如,fileop -c src dst -m src dst -d src)
- 帶有多個(gè)子命令的單個(gè)命令(例如,fileop copy src dst, fileop move src dst, fileop delete src)
2. 使用標(biāo)志、參數(shù)、子命令和選項(xiàng)
**標(biāo)志(flag)**是以一個(gè)或多個(gè)(通常是2個(gè))中劃線(-)開(kāi)頭的輸入?yún)?shù),它可以修改CLI程序的行為或輸出。例如:
$curl -s -o output.txt https://cdxwcx.com
在這個(gè)例子中:
- “-s”是一個(gè)讓curl沉默的標(biāo)志,即不輸出執(zhí)行日志到控制臺(tái);
- “-o”是另一個(gè)標(biāo)志,用于指定輸出文件的名稱
- “output.txt”則是一個(gè)參數(shù),是為“-o”標(biāo)志提供的值。
**參數(shù)(argument)**是不以中劃線(-)開(kāi)頭的輸入?yún)?shù),為你的CLI程序提供額外的信息或數(shù)據(jù)。例如:
$tar xvf archive.tar.gz
我們看在這個(gè)例子中:
- x是一個(gè)指定提取模式的參數(shù)
- v是一個(gè)參數(shù),指定的是輸出內(nèi)容的詳細(xì)(verbose)程度
- f是另一個(gè)參數(shù),用于指定采用的是文件模式,即將壓縮結(jié)果輸出到一個(gè)文件或從一個(gè)壓縮文件讀取數(shù)據(jù)
- archive.tar.gz是一個(gè)參數(shù),提供文件名。
**子命令(subcommand)**是輸入?yún)?shù),作為主命令下的輔助命令。它們通常有自己的一組標(biāo)志和參數(shù)。比如下面例子:
$git commit -m "Initial commit"
我們看在這個(gè)例子中:
- git是主命令(primary command)
- commit是一個(gè)子命令,用于從staged的修改中創(chuàng)建一個(gè)新的提交(commit)
- “-m”是commit子命令的一個(gè)標(biāo)志,用于指定提交信息
- "Initial commit"是commit子命令的一個(gè)參數(shù),為"-m"標(biāo)志提供值。
**選項(xiàng)(option)**是輸入?yún)?shù),它可以使用等號(hào)(=)將標(biāo)志和參數(shù)合并為一個(gè)參數(shù)。例如:
$docker run --name=my-container ubuntu:latest
我們看在這個(gè)例子中“--name=my-container”是一個(gè)選項(xiàng),它將容器的名稱設(shè)為my-container。該選項(xiàng)前面的部分“--name”是一個(gè)標(biāo)志,后面的部分“my-container”是參數(shù)。
3. 使用cobra包等來(lái)解析和驗(yàn)證用戶輸入的信息
如果手工來(lái)解析和驗(yàn)證用戶輸入的信息,既繁瑣又容易出錯(cuò)。幸運(yùn)的是,有許多庫(kù)和框架可以幫助你在Go中解析和驗(yàn)證用戶輸入。其中最流行的是cobra[16]。
cobra是一個(gè)Go包,它提供了簡(jiǎn)單的接口來(lái)創(chuàng)建強(qiáng)大的CLI程序。它支持子命令、標(biāo)志、參數(shù)、選項(xiàng)、環(huán)境變量和配置文件。它還能很好地與其他庫(kù)集成,比如:viper[17](用于配置管理)、pflag[18](用于POSIX/GNU風(fēng)格的標(biāo)志)和Docopt[19](用于生成文檔)。
另一個(gè)不那么流行但卻提供了一種聲明式的方法來(lái)創(chuàng)建優(yōu)雅的CLI程序的包是Kingpin[20],它支持標(biāo)志、參數(shù)、選項(xiàng)、環(huán)境變量和配置文件。它還具有自動(dòng)幫助生成、命令完成、錯(cuò)誤處理和類型轉(zhuǎn)換等功能。
cobra和Kingpin在其官方網(wǎng)站上都有大量的文檔和例子,你可以根據(jù)你的偏好和需要選擇任選其一。
4. 遵循POSIX慣例和GNU擴(kuò)展的CLI語(yǔ)法
POSIX(Portable Operating System Interface)[21]是一套標(biāo)準(zhǔn),定義了軟件應(yīng)該如何與操作系統(tǒng)進(jìn)行交互。其中一個(gè)標(biāo)準(zhǔn)定義了CLI程序的語(yǔ)法和語(yǔ)義。GNU(GNU's Not Unix)是一個(gè)旨在創(chuàng)建一個(gè)與UNIX兼容的自由軟件操作系統(tǒng)的項(xiàng)目。GNU下的一個(gè)子項(xiàng)目是GNU Coreutils[22],它提供了許多常見(jiàn)的CLI程序,如ls、cp、mv等。
POSIX和GNU都為CLI語(yǔ)法建立了一些約定和擴(kuò)展,許多CLI程序都采用了這些約定與擴(kuò)展。下面列舉了這些約定和擴(kuò)展中的一些主要內(nèi)容:
- 單字母標(biāo)志(single-letter flag)以一個(gè)中劃線(-)開(kāi)始,可以組合在一起(例如:-a -b -c 或 -abc )
- 長(zhǎng)標(biāo)志(long flag)以兩個(gè)中劃線(--)開(kāi)頭,但不能組合在一起(例如:--all、--backup、--color )
- 選項(xiàng)使用等號(hào)(=)來(lái)分隔標(biāo)志名和參數(shù)值(例如:--name=my-container )
- 參數(shù)跟在標(biāo)志或選項(xiàng)之后,沒(méi)有任何分隔符(例如:curl -o output.txt https://cdxwcx.com )。
- 子命令跟在主命令之后,沒(méi)有任何分隔符(例如:git commit -m "Initial commit" )
- 一個(gè)雙中劃線(--)表示標(biāo)志或選項(xiàng)的結(jié)束和參數(shù)的開(kāi)始(例如:rm -- -f 表示要?jiǎng)h除“-f”這個(gè)文件,由于雙破折線的存在,這里的“-f”不再是標(biāo)志)
遵循這些約定和擴(kuò)展可以使你的CLI程序更加一致、直觀,并與其他CLI程序兼容。然而,它們并不是強(qiáng)制性的,如果你有充分的理由,你也大可不必完全遵守它們。例如,一些CLI程序使用斜線(/)而不是中劃線(-)表示標(biāo)志(例如, robocopy /S /E src dst )。
四. 處理錯(cuò)誤和信號(hào)
編寫(xiě)好的CLI程序的一個(gè)重要環(huán)節(jié)就是**優(yōu)雅地處理錯(cuò)誤和信號(hào)[23]**。
錯(cuò)誤是指你的程序由于某些內(nèi)部或外部因素而無(wú)法執(zhí)行其預(yù)定功能的情況。信號(hào)是由操作系統(tǒng)或其他進(jìn)程向你的程序發(fā)送的事件,以通知它一些變化或請(qǐng)求。在這一節(jié)中,我將說(shuō)明一下如何使用log、fmt和errors包進(jìn)行日志輸出和錯(cuò)誤處理,如何使用os.Exit和defer語(yǔ)句進(jìn)行優(yōu)雅的終止,如何使用os.Signal和context包進(jìn)行中斷和取消操作,以及如何遵循CLI程序的退出狀態(tài)代碼慣例。
1. 使用log、fmt和errors包進(jìn)行日志記錄和錯(cuò)誤處理
Go標(biāo)準(zhǔn)庫(kù)中有三個(gè)包log、fmt和errors可以幫助你進(jìn)行日志和錯(cuò)誤處理。log包提供了一個(gè)簡(jiǎn)單的接口,可以將格式化的信息寫(xiě)到標(biāo)準(zhǔn)輸出或文件中。fmt包則提供了各種格式化字符串和值的函數(shù)。errors包提供了創(chuàng)建和操作錯(cuò)誤值的函數(shù)。
要使用log包,你需要在你的代碼中導(dǎo)入它:
import "log"
然后你可以使用log.Println、log.Printf、log.Fatal和log.Fatalf等函數(shù)來(lái)輸出不同嚴(yán)重程度的信息。比如說(shuō):
log.Println("Starting the program...") // 打印帶有時(shí)間戳的消息
log.Printf("Processing file %s...\n", filename) // 打印一個(gè)帶時(shí)間戳的格式化信息
log.Fatal("Cannot open file: ", err) // 打印一個(gè)帶有時(shí)間戳的錯(cuò)誤信息并退出程序
log.Fatalf("Invalid input: %v\n", input) // 打印一個(gè)帶時(shí)間戳的格式化錯(cuò)誤信息,并退出程序。為了使用fmt包,你需要先在你的代碼中導(dǎo)入它:
import "fmt"
然后你可以使用fmt.Println、fmt.Printf、fmt.Sprintln、fmt.Sprintf等函數(shù)以各種方式格式化字符串和值。比如說(shuō):
fmt.Println("Hello world!") // 打印一條信息,后面加一個(gè)換行符
fmt.Printf("The answer is %d\n", 42) // 打印一條格式化的信息,后面是換行。
s := fmt.Sprintln("Hello world!") // 返回一個(gè)帶有信息和換行符的字符串。
t := fmt.Sprintf("The answer is %d\n", 42) // 返回一個(gè)帶有格式化信息和換行的字符串。要使用錯(cuò)誤包,你同樣需要在你的代碼中導(dǎo)入它:
import "errors"
然后你可以使用 errors.New、errors.Unwrap、errors.Is等函數(shù)來(lái)創(chuàng)建和操作錯(cuò)誤值。比如說(shuō):
err := errors.New("Something went wrong") // 創(chuàng)建一個(gè)帶有信息的錯(cuò)誤值
cause := errors.Unwrap(err) // 返回錯(cuò)誤值的基本原因(如果沒(méi)有則為nil)。
match := errors.Is(err, io.EOF) // 如果一個(gè)錯(cuò)誤值與另一個(gè)錯(cuò)誤值匹配,則返回真(否則返回假)。2. 使用os.Exit和defer語(yǔ)句實(shí)現(xiàn)CLI程序的優(yōu)雅終止
Go有兩個(gè)功能可以幫助你優(yōu)雅地終止CLI程序:os.Exit和defer。os.Exit函數(shù)立即退出程序,并給出退出狀態(tài)代碼。defer語(yǔ)句則會(huì)在當(dāng)前函數(shù)退出前執(zhí)行一個(gè)函數(shù)調(diào)用,它常用來(lái)執(zhí)行清理收尾動(dòng)作,如關(guān)閉文件或釋放資源。
要使用os.Exit函數(shù),你需要在你的代碼中導(dǎo)入os包:
import "os"
然后你可以使用os.Exit函數(shù),它的整數(shù)參數(shù)代表退出狀態(tài)代碼。比如說(shuō)
os.Exit(0) // 以成功的代碼退出程序
os.Exit(1) // 以失敗代碼退出程序
要使用defer語(yǔ)句,你需要把它寫(xiě)在你想后續(xù)執(zhí)行的函數(shù)調(diào)用之前。比如說(shuō)
file, err := os.Open(filename) // 打開(kāi)一個(gè)文件供讀取。
if err != nil {
log.Fatal(err) // 發(fā)生錯(cuò)誤時(shí)退出程序
}
defer file.Close() // 在函數(shù)結(jié)束時(shí)關(guān)閉文件。
// 對(duì)文件做一些處理...
3. 使用os.signal和context包來(lái)實(shí)現(xiàn)中斷和取消操作
Go有兩個(gè)包可以幫助你實(shí)現(xiàn)中斷和取消長(zhǎng)期運(yùn)行的或阻塞的操作,它們是os.signal和context包。os.signal提供了一種從操作系統(tǒng)或其他進(jìn)程接收信號(hào)的方法。context包提供了一種跨越API邊界傳遞取消信號(hào)和deadline的方法。
要使用os.signal,你需要先在你的代碼中導(dǎo)入它。
import (
"os"
"os/signal"
)
然后你可以使用signal.Notify函數(shù)針對(duì)感興趣的信號(hào)(如下面的os.Interrupt信號(hào))注冊(cè)一個(gè)接收channel(sig)。比如說(shuō):
sig := make(chan os.Signal, 1) // 創(chuàng)建一個(gè)帶緩沖的信號(hào)channel。
signal.Notify(sig, os.Interrupt) // 注冊(cè)sig以接收中斷信號(hào)(例如Ctrl-C)。
// 做一些事情...
select {
case <-sig: // 等待來(lái)自sig channel的信號(hào)
fmt.Println("被用戶中斷了")
os.Exit(1) // 以失敗代碼退出程序。
default: //如果沒(méi)有收到信號(hào)就執(zhí)行
fmt.Println("成功完成")
os.Exit(0) // 以成功代碼退出程序。
}
要使用上下文包,你需要在你的代碼中導(dǎo)入它:
import "context"
然后你可以使用它的函數(shù),如context.Background、context.WithCancel、context.WithTimeout等來(lái)創(chuàng)建和管理Context。Context是一個(gè)攜帶取消信號(hào)和deadline的對(duì)象,可以跨越API邊界。比如說(shuō):
ctx := context.Background() // 創(chuàng)建一個(gè)空的背景上下文(從不取消)。
ctx, cancel := context.WithCancel(ctx) // 創(chuàng)建一個(gè)新的上下文,可以通過(guò)調(diào)用cancel函數(shù)來(lái)取消。
defer cancel() // 在函數(shù)結(jié)束前執(zhí)行ctx的取消動(dòng)作
// 將ctx傳遞給一些接受它作為參數(shù)的函數(shù)......
select {
case <-ctx.Done(): // 等待來(lái)自ctx的取消信號(hào)
fmt.Println("Canceled by parent")
return ctx.Err() // 從ctx返回一個(gè)錯(cuò)誤值
default: // 如果沒(méi)有收到取消信號(hào)就執(zhí)行
fmt.Println("成功完成")
return nil // 不返回錯(cuò)誤值
}
4. CLI程序的退出狀態(tài)代碼慣例
退出狀態(tài)代碼是一個(gè)整數(shù),表示CLI程序是否成功執(zhí)行完成。CLI程序通過(guò)調(diào)用os.Exit或從main返回的方式返回退出狀態(tài)值。其他CLI程序或腳本可以可以檢查這些退出狀態(tài)碼,并根據(jù)狀態(tài)碼值的不同執(zhí)行不同的處理操作。
業(yè)界有一些關(guān)于退出狀態(tài)代碼的約定和擴(kuò)展,這些約定被許多CLI程序廣泛采用。其中一些主要的約定和擴(kuò)展如下:。
- 退出狀態(tài)代碼為0表示程序執(zhí)行成功(例如:os.Exit(0) )
- 非零的退出狀態(tài)代碼表示失?。ɡ纾簅s.Exit(1) )。
- 不同的非零退出狀態(tài)代碼可能表示不同的失敗類型或原因(例如:os.Exit(2)表示使用錯(cuò)誤,os.Exit(3)表示權(quán)限錯(cuò)誤等等)。
- 大于125的退出狀態(tài)代碼可能表示被外部信號(hào)終止(例如,os.Exit(130)為被信號(hào)中斷)。
遵循這些約定和擴(kuò)展可以使你的CLI程序表現(xiàn)的更加一致、可靠并與其他CLI程序兼容。然而,它們不是強(qiáng)制性的,你可以使用任何對(duì)你的程序有意義的退出狀態(tài)代碼。例如,一些CLI程序使用高于200的退出狀態(tài)代碼來(lái)表示自定義或特定應(yīng)用的錯(cuò)誤(例如,os.Exit(255)表示未知錯(cuò)誤)。
五. 編寫(xiě)文檔
編寫(xiě)優(yōu)秀CLI程序的另一個(gè)重要環(huán)節(jié)是編寫(xiě)清晰簡(jiǎn)潔的文檔,解釋你的程序做什么以及如何使用它。文檔可以采取各種形式,如README文件、usage信息、help flag等。在本節(jié)中,我們將告訴你如何為你的程序?qū)懸粋€(gè)README文件,如何為你的程序?qū)懸粋€(gè)有用的usage和help flag等。
1. 為你的CLI程序?qū)懸粋€(gè)清晰簡(jiǎn)潔的README文件
README文件是一個(gè)文本文件,它提供了關(guān)于你的程序的基本信息,如它的名稱、描述、用法、安裝、依賴性、許可證和聯(lián)系細(xì)節(jié)等。它通常是用戶或開(kāi)發(fā)者在源代碼庫(kù)或軟件包管理器上首次使用你的程序時(shí)會(huì)看到的內(nèi)容。
如果你要為Go CLI程序編寫(xiě)一個(gè)優(yōu)秀的README文件,你應(yīng)該遵循一些最佳實(shí)踐,比如:
- 使用一個(gè)描述性的、醒目的標(biāo)題,反映你的程序的目的和功能。
- 提供一個(gè)簡(jiǎn)短的介紹,解釋你的程序是做什么的,為什么它是有用的或獨(dú)特的。
- 包括一個(gè)usage部分,說(shuō)明如何用不同的標(biāo)志、參數(shù)、子命令和選項(xiàng)來(lái)調(diào)用你的程序。你可以使用代碼塊或屏幕截圖來(lái)說(shuō)明這些例子。
- 包括一個(gè)安裝(install)部分,解釋如何在不同的平臺(tái)上下載和安裝你的程序。你可以使用go install、go get、goreleaser[24]或其他工具來(lái)簡(jiǎn)化這一過(guò)程。
- 指定你的程序的發(fā)行許可,并提供一個(gè)許可全文的鏈接。你可以使用SPDX標(biāo)識(shí)符[25]來(lái)表示許可證類型。
- 為想要報(bào)告問(wèn)題、請(qǐng)求新功能、貢獻(xiàn)代碼或提問(wèn)的用戶或開(kāi)發(fā)者提供聯(lián)系信息。你可以使用github issue、pr、discussion、電子郵件或其他渠道來(lái)達(dá)到這個(gè)目的。
以下是一個(gè)Go CLI程序的README文件的示例供參考:
2. 為你的CLI程序編寫(xiě)有用的usage和help標(biāo)志
usage信息是一段簡(jiǎn)短的文字,總結(jié)了如何使用你的程序及其可用的標(biāo)志、參數(shù)、子命令和選項(xiàng)。它通常在你的程序在沒(méi)有參數(shù)或輸入無(wú)效的情況下運(yùn)行時(shí)顯示。
help標(biāo)志是一個(gè)特殊的標(biāo)志(通常是-h或--help),它可以觸發(fā)顯示使用信息和一些關(guān)于你的程序的額外信息。
為了給你的Go CLI程序?qū)懹杏玫膗sage信息和help標(biāo)志,你應(yīng)該遵循一些準(zhǔn)則,比如說(shuō):
- 使用一致而簡(jiǎn)潔的語(yǔ)法來(lái)描述標(biāo)志、參數(shù)、子命令和選項(xiàng)。你可以用方括號(hào)“[ ]”表示可選元素,使用角括號(hào)“< >”表示必需元素,使用省略號(hào)“...”表示重復(fù)元素,使用管道“|”表示備選,使用中劃線“-”表示標(biāo)志(flag),使用等號(hào)“=”表示標(biāo)志的值等等。
- 對(duì)標(biāo)志、參數(shù)、子命令和選項(xiàng)應(yīng)使用描述性的名稱,以反映其含義和功能。避免使用單字母名稱,除非它們非常常見(jiàn)或非常直觀(如-v按慣例表示verbose模式)。
- 為每個(gè)標(biāo)志、參數(shù)、子命令和選項(xiàng)提供簡(jiǎn)短而清晰的描述,解釋它們的作用以及它們?nèi)绾斡绊懩愕某绦虻男袨椤D憧梢杂脠A括號(hào)“( )”來(lái)表達(dá)額外的細(xì)節(jié)或例子。
- 使用標(biāo)題或縮進(jìn)將相關(guān)的標(biāo)志、參數(shù)、子命令和選項(xiàng)組合在一起。你也可以用空行或水平線(---)來(lái)分隔usage的不同部分。
- 在每組中按名稱的字母順序排列標(biāo)志。在每組中按重要性或邏輯順序排列參數(shù)。在每組中按使用頻率排列子命令。
git的usage就是一個(gè)很好的例子:
$git
usage: git [--version] [--help] [-C] [-c = ]
[--exec-path[=]] [--html-path] [--man-path] [--info-path]
[-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
[--git-dir=] [--work-tree= ] [--namespace= ]
[ ]
結(jié)合上面的準(zhǔn)則,大家可以細(xì)心體會(huì)一下。
六. 測(cè)試和發(fā)布你的CLI程序
編寫(xiě)優(yōu)秀CLI程序的最后一個(gè)環(huán)節(jié)是測(cè)試和發(fā)布你的程序。測(cè)試確保你的程序可以按預(yù)期工作,并符合質(zhì)量標(biāo)準(zhǔn)。發(fā)布可以使你的程序可供用戶使用和訪問(wèn)。
在本節(jié)中,我將說(shuō)明如何使用testing、testify/assert、mock包對(duì)你的代碼進(jìn)行單元測(cè)試,如何使用go test、coverage、benchmark工具來(lái)運(yùn)行測(cè)試和測(cè)量程序性能以及如何使用goreleaser包來(lái)構(gòu)建跨平臺(tái)的二進(jìn)制文件。
1. 使用testing、testify的assert及mock包對(duì)你的代碼進(jìn)行單元測(cè)試
單元測(cè)試是一種驗(yàn)證單個(gè)代碼單元(如函數(shù)、方法或類型)的正確性和功能的技術(shù)。單元測(cè)試可以幫助你盡早發(fā)現(xiàn)錯(cuò)誤,提高代碼質(zhì)量和可維護(hù)性,并促進(jìn)重構(gòu)和調(diào)試。
要為你的Go CLI程序編寫(xiě)單元測(cè)試,你應(yīng)該遵循一些最佳實(shí)踐:
- 使用內(nèi)置的測(cè)試包來(lái)創(chuàng)建測(cè)試函數(shù),以Test開(kāi)頭,后面是被測(cè)試的函數(shù)或方法的名稱。例如:func TestSum(t *testing.T) { ... };
- 使用*testing.T類型的t參數(shù),使用t.Error、t.Errorf、t.Fatal或t.Fatalf這樣的方法報(bào)告測(cè)試失敗。你也可以使用t.Log、t.Logf、t.Skip或t.Skipf這樣的方法來(lái)提供額外的信息或有條件地跳過(guò)測(cè)試。
- 使用Go子測(cè)試(sub test)[26],通過(guò)t.Run方法將相關(guān)的測(cè)試分組。例如:
func TestSum(t *testing.T) {
t.Run("positive numbers", func(t *testing.T) {
// test sum with positive numbers
})
t.Run("negative numbers", func(t *testing.T) {
// test sum with negative numbers
})
}- 使用表格驅(qū)動(dòng)(table-driven)的測(cè)試來(lái)運(yùn)行多個(gè)測(cè)試用例,比如下面的例子:
func TestSum(t *testing.T) {
tests := []struct{
name string
a int
b int
want int
}{
{"positive numbers", 1, 2, 3},
{"negative numbers", -1, -2, -3},
{"zero", 0, 0 ,0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Sum(tt.a , tt.b)
if got != tt.want {
t.Errorf("Sum(%d , %d) = %d; want %d", tt.a , tt.b , got , tt.want)
}
})
}
}- 使用外部包,如testify/assert或mock來(lái)簡(jiǎn)化你的斷言或?qū)ν獠康囊蕾囆?。比如說(shuō):
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type Calculator interface {
Sum(a int , b int) int
}
type MockCalculator struct {
mock.Mock
}
func (m *MockCalculator) Sum(a int , b int) int {
args := m.Called(a , b)
return args.Int(0)
}
2. 使用Go的測(cè)試、覆蓋率、性能基準(zhǔn)工具來(lái)運(yùn)行測(cè)試和測(cè)量性能
Go提供了一套工具來(lái)運(yùn)行測(cè)試和測(cè)量你的代碼的性能。你可以使用這些工具來(lái)確保你的代碼按預(yù)期工作,檢測(cè)錯(cuò)誤或bug,并優(yōu)化你的代碼以提高速度和效率。
要使用go test、coverage、benchmark工具來(lái)運(yùn)行測(cè)試和測(cè)量你的Go CLI程序的性能,你應(yīng)該遵循一些步驟,比如說(shuō)。
- 將以_test.go結(jié)尾的測(cè)試文件寫(xiě)在與被測(cè)試代碼相同的包中。例如:sum_test.go用于測(cè)試sum.go。
- 使用go測(cè)試命令來(lái)運(yùn)行一個(gè)包中的所有測(cè)試或某個(gè)特定的測(cè)試文件。你也可以使用一些標(biāo)志,如-v,用于顯示verbose的輸出,-run用于按名字過(guò)濾測(cè)試用例,-cover用于顯示代碼覆蓋率,等等。例如:go test -v -cover ./...
- 使用go工具cover命令來(lái)生成代碼覆蓋率的HTML報(bào)告,并高亮顯示代碼行。你也可以使用-func這樣的標(biāo)志來(lái)顯示函數(shù)的代碼覆蓋率,用-html還可以在瀏覽器中打開(kāi)覆蓋率結(jié)果報(bào)告等等。例如:go tool cover -html=coverage.out
- 編寫(xiě)性能基準(zhǔn)函數(shù),以Benchmark開(kāi)頭,后面是被測(cè)試的函數(shù)或方法的名稱。使用類型為*testing.B的參數(shù)b來(lái)控制迭代次數(shù),并使用b.N、b.ReportAllocs等方法控制報(bào)告結(jié)果的輸出。比如說(shuō)
func BenchmarkSum(b *testing.B) {
for i := 0; i < b.N; i++ {
Sum(1 , 2)
}
}- 使用go test -bench命令來(lái)運(yùn)行一個(gè)包中的所有性能基準(zhǔn)測(cè)試或某個(gè)特定的基準(zhǔn)文件。你也可以使用-benchmem這樣的標(biāo)志來(lái)顯示內(nèi)存分配的統(tǒng)計(jì)數(shù)據(jù),-cpuprofile或-memprofile來(lái)生成CPU或內(nèi)存profile文件等等。例如:go test -bench . -benchmem ./...
- 使用pprof或benchstat等工具來(lái)分析和比較CPU或內(nèi)存profile文件或基準(zhǔn)測(cè)試結(jié)果。比如說(shuō)。
# Generate CPU profile
go test -cpuprofile cpu.out ./...
# Analyze CPU profile using pprof
go tool pprof cpu.out
# Generate two sets of benchmark results
go test -bench . ./... > old.txt
go test -bench . ./... > new.txt
# Compare benchmark results using benchstat
benchstat old.txt new.txt
3. 使用goreleaser包構(gòu)建跨平臺(tái)的二進(jìn)制文件
構(gòu)建跨平臺(tái)二進(jìn)制文件意味著將你的代碼編譯成可執(zhí)行文件,可以在不同的操作系統(tǒng)和架構(gòu)上運(yùn)行,如Windows、Linux、Mac OS、ARM等。這可以幫助你向更多的人分發(fā)你的程序,使用戶更容易安裝和運(yùn)行你的程序而不需要任何依賴或配置。
為了給你的Go CLI程序建立跨平臺(tái)的二進(jìn)制文件,你可以使用外部軟件包,比如goreleaser等 ,它們可以自動(dòng)完成程序的構(gòu)建、打包和發(fā)布過(guò)程。下面是使用goreleaser包構(gòu)建程序的一些步驟。
- 使用go get或go install命令安裝goreleaser。例如: go install github.com/goreleaser/goreleaser@latest
- 創(chuàng)建一個(gè)配置文件(通常是.goreleaser.yml),指定如何構(gòu)建和打包你的程序。你可以定制各種選項(xiàng),如二進(jìn)制名稱、版本、主文件、輸出格式、目標(biāo)平臺(tái)、壓縮、校驗(yàn)和、簽名等。例如。
# .goreleaser.yml
project_name: mycli
builds:
- main: ./cmd/mycli/main.go
binary: mycli
goos:
- windows
- darwin
- linux
goarch:
- amd64
- arm64
archives:
- format: zip
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- LICENSE.txt
- README.md
checksum:
name_template: "{{ .ProjectName }}_checksums.txt"
algorithm: sha256
運(yùn)行g(shù)oreleaser命令,根據(jù)配置文件構(gòu)建和打包你的程序。你也可以使用-snapshot用于測(cè)試,-release-notes用于從提交信息中生成發(fā)布說(shuō)明,-rm-dist用于刪除之前的構(gòu)建,等等。例如:goreleaser --snapshot --rm-dist。
檢查輸出文件夾(通常是dist)中生成的二進(jìn)制文件和其他文件。你也可以使用goreleaser的發(fā)布功能將它們上傳到源代碼庫(kù)或軟件包管理器中。
七. clig.dev指南要點(diǎn)
通過(guò)上述的系統(tǒng)說(shuō)明,你現(xiàn)在應(yīng)該可以設(shè)計(jì)并使用Go實(shí)現(xiàn)出一個(gè)CLI程序了。不過(guò)本文并非覆蓋了clig.dev指南的所有要點(diǎn),因此,在結(jié)束本文之前,我們?cè)賮?lái)回顧一下clig.dev指南中的要點(diǎn),大家再體會(huì)一下。
前面說(shuō)過(guò),clig.dev上的cli指南是一個(gè)開(kāi)源指南,可以幫助你寫(xiě)出更好的命令行程序,它采用了傳統(tǒng)的UNIX原則,并針對(duì)現(xiàn)代的情況進(jìn)行了更新。
遵循cli準(zhǔn)則的一些好處是:
- 你可以創(chuàng)建易于使用、理解和記憶的CLI程序。
- 你可以設(shè)計(jì)出能與其他程序進(jìn)行很好配合的CLI程序,并遵循共同的慣例。
- 你可以避免讓用戶和開(kāi)發(fā)者感到沮喪的常見(jiàn)陷阱和錯(cuò)誤。
- 你可以從其他CLI設(shè)計(jì)者和用戶的經(jīng)驗(yàn)和智慧中學(xué)習(xí)。
下面是該指南的一些要點(diǎn):
- 理念
這一部分解釋了好的CLI設(shè)計(jì)背后的核心原則,如人本設(shè)計(jì)、可組合性、可發(fā)現(xiàn)性、對(duì)話性等。例如,以人為本的設(shè)計(jì)意味著CLI程序?qū)θ祟悂?lái)說(shuō)應(yīng)該易于使用和理解,而不僅僅是機(jī)器??山M合性意味著CLI程序應(yīng)該通過(guò)遵循共同的慣例和標(biāo)準(zhǔn)與其他程序很好地協(xié)作。
- 參數(shù)和標(biāo)志
這一部分講述了如何在你的CLI程序中使用位置參數(shù)(positional arguments )和標(biāo)志。它還解釋了如何處理默認(rèn)值、必傳參數(shù)、布爾標(biāo)志、多值等。例如,你應(yīng)該對(duì)命令的主要對(duì)象或動(dòng)作使用位置參數(shù),對(duì)修改或可選參數(shù)使用標(biāo)志。你還應(yīng)該使用長(zhǎng)短兩種形式的標(biāo)志(如-v或-verbose),并遵循常見(jiàn)的命名模式(如--help或--version)。
- 配置
這部分介紹了如何使用配置文件和環(huán)境變量來(lái)為你的CLI程序存儲(chǔ)持久的設(shè)置。它還解釋了如何處理配置選項(xiàng)的優(yōu)先級(jí)、驗(yàn)證、文檔等。例如,你應(yīng)該使用配置文件來(lái)處理用戶很少改變的設(shè)置,或者是針對(duì)某個(gè)項(xiàng)目或環(huán)境的設(shè)置。對(duì)于特定于環(huán)境或會(huì)話的設(shè)置(如憑證或路徑),你也應(yīng)該使用環(huán)境變量。
- 輸出
這部分介紹了如何格式化和展示你的CLI程序的輸出。它還解釋了如何處理輸出verbose級(jí)別、進(jìn)度指示器、顏色、表格等。例如,你應(yīng)該使用標(biāo)準(zhǔn)輸出(stdout)進(jìn)行正常的輸出,這樣輸出的信息可以通過(guò)管道輸送到其他程序或文件。你還應(yīng)該使用標(biāo)準(zhǔn)錯(cuò)誤(stderr)來(lái)處理不屬于正常輸出流的錯(cuò)誤或警告。
- 錯(cuò)誤
這部分介紹了如何在你的CLI程序中優(yōu)雅地處理錯(cuò)誤。它還解釋了如何使用退出狀態(tài)碼、錯(cuò)誤信息、堆棧跟蹤等。例如,你應(yīng)該使用表明錯(cuò)誤類型的退出代碼(如0代表成功,1代表一般錯(cuò)誤)。你還應(yīng)該使用簡(jiǎn)潔明了的錯(cuò)誤信息,解釋出錯(cuò)的原因以及如何解決。
- 子命令
這部分介紹了當(dāng)CLI程序有多種操作或操作模式時(shí),如何在CLI程序中使用子命令。它還解釋了如何分層構(gòu)建子命令,組織幫助文本,以及處理常見(jiàn)的子命令(如help或version)。例如,當(dāng)你的程序有不同的功能,需要不同的參數(shù)或標(biāo)志時(shí)(如git clone或git commit),你應(yīng)該使用子命令。你還應(yīng)該提供一個(gè)默認(rèn)的子命令,或者在沒(méi)有給出子命令時(shí)提供一個(gè)可用的子命令列表。
業(yè)界有許多精心設(shè)計(jì)的CLI工具的例子,它們都遵循cli準(zhǔn)則,大家可以通過(guò)使用來(lái)深刻體會(huì)一下這些準(zhǔn)則。下面是一些這樣的CLI工具的例子:
- httpie:一個(gè)命令行HTTP客戶端,具有直觀的UI,支持JSON,語(yǔ)法高亮,類似wget的下載,插件等功能。例如,Httpie使用清晰簡(jiǎn)潔的語(yǔ)法進(jìn)行HTTP請(qǐng)求,支持多種輸出格式和顏色,優(yōu)雅地處理錯(cuò)誤并提供有用的文檔。
- git:一個(gè)分布式的版本控制系統(tǒng),讓你管理你的源代碼并與其他開(kāi)發(fā)者合作。例如,Git使用子命令進(jìn)行不同的操作(如git clone或git commit),遵循通用的標(biāo)志(如-v或-verbose),提供有用的反饋和建議(如git status或git help),并支持配置文件和環(huán)境變量。
- npm:一個(gè)JavaScript的包管理器,讓你為你的項(xiàng)目安裝和管理依賴性。例如,NPM使用一個(gè)簡(jiǎn)單的命令結(jié)構(gòu)(npm [args]),提供一個(gè)簡(jiǎn)潔的初始幫助信息,有更詳細(xì)的選項(xiàng)(npm help npm),支持標(biāo)簽完成和合理的默認(rèn)值,并允許你通過(guò)配置文件(.npmrc)自定義設(shè)置。
八. 小結(jié)
在這篇文章中,我們系統(tǒng)說(shuō)明了如何編寫(xiě)出遵循命令行接口指南的Go CLI程序。
你學(xué)習(xí)了如何設(shè)置Go環(huán)境、設(shè)計(jì)命令行接口、處理錯(cuò)誤和信號(hào)、編寫(xiě)文檔、使用各種工具和軟件包測(cè)試和發(fā)布程序。你還看到了一些代碼和配置文件的例子。通過(guò)遵循這些準(zhǔn)則和最佳實(shí)踐,你可以創(chuàng)建一個(gè)用戶友好、健壯和可靠的CLI程序。
最后我們回顧了clig.dev的指南要點(diǎn),希望你能更深刻理解這些要點(diǎn)的含義。
我希望你喜歡這篇文章并認(rèn)為它很有用。如果你有任何問(wèn)題或反饋,請(qǐng)隨時(shí)聯(lián)系我。編碼愉快!
注:本文系與New Bing Chat聯(lián)合完成,旨在驗(yàn)證如何基于AIGC能力構(gòu)思和編寫(xiě)長(zhǎng)篇文章。文章內(nèi)容的正確性經(jīng)過(guò)筆者全面審校,可放心閱讀。
本文轉(zhuǎn)載自微信公眾號(hào)「 白明的贊賞賬戶」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系 白明的贊賞賬戶公眾號(hào)。
分享標(biāo)題:Go開(kāi)發(fā)命令行程序指南
網(wǎng)頁(yè)網(wǎng)址:http://www.dlmjj.cn/article/dpojghc.html


咨詢
建站咨詢
