一個(gè)庫(kù),用Go搞定命令行程序
我們平時(shí)做的Go項(xiàng)目除了寫的各種API接口外,還經(jīng)常會(huì)寫任務(wù)腳本、命令行程序、定時(shí)任務(wù)等,其實(shí)這幾個(gè)是一個(gè)東西,你寫的任務(wù)腳本支持接受指令傳參,那它不就是命令行程序了?再把程序部署到服務(wù)器用Go Cron加個(gè)任務(wù)就是定時(shí)任務(wù)了。
圖片
Go 官方有一個(gè) flags 庫(kù)提供了最基礎(chǔ)的命令行參數(shù)支持,不過(guò)確實(shí)不好用,今天帶你認(rèn)識(shí)一個(gè)超贊的庫(kù)——urfave/cli,它能讓你用一種簡(jiǎn)單優(yōu)雅的方式來(lái)構(gòu)建命令行程序。
什么是urfave/cli?
urfave/cli 是一個(gè)用 Go 編寫的、簡(jiǎn)單、快速且有趣的庫(kù),用于構(gòu)建命令行應(yīng)用程序。無(wú)論是小工具還是復(fù)雜的大型 CLI 程序,它都能輕松應(yīng)對(duì)。它的設(shè)計(jì)哲學(xué)是讓我們用聲明式的方式來(lái)定義命令、子命令、標(biāo)志(Flags),然后它會(huì)自動(dòng)幫你處理參數(shù)解析、幫助文檔生成等所有繁瑣的工作,聽(tīng)起來(lái)是不是很棒?
安裝
運(yùn)行以下命令來(lái)安裝 urfave/cli 的 v2 版本:
go get github.com/urfave/cli/v2第一個(gè) CLI 程序
我們從經(jīng)典的 "Hello, World!" 開(kāi)始,創(chuàng)建一個(gè) main.go 文件,然后敲入以下代碼:
package main
import (
"log"
"os"
"github.com/urfave/cli/v2"
)
func main() {
app := &cli.App{
Name: "greet",
Usage: "向世界打個(gè)招呼!",
Action: func(c *cli.Context) error {
println("Hello, world!")
returnnil
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}運(yùn)行命令程序
go run main.go你會(huì)看到終端輸出了 Hello, world!,當(dāng)然我們也可以 build 后用真正的命令去運(yùn)行
# build
go build -o greet ./main.go
# 運(yùn)行命令
./greeturfave/cli 自動(dòng)為我們生成了幫助信息。上面這個(gè)命令運(yùn)行時(shí)添加 --help 就能在控制臺(tái)輸出幫助信息。
添加命令行傳參
只會(huì)說(shuō) "Hello, world!" 可不夠,我們希望它能跟指定的人打招呼。這就要用到“標(biāo)志”(Flags)了。
我們來(lái)修改一下代碼,添加一個(gè) --name的標(biāo)志:
package main
import (
"fmt"
"log"
"os"
"github.com/urfave/cli/v2"
)
func main() {
app := &cli.App{
Name: "greet",
Usage: "向世界或某人打個(gè)招呼!",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Value: "world", // 默認(rèn)值
Usage: "指定打招呼的對(duì)象",
Aliases: []string{"n"}, // 別名,-n 等同于 --name
},
},
Action: func(c *cli.Context) error {
name := c.String("name")
fmt.Printf("Hello, %s!\n", name)
returnnil
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}現(xiàn)在重新打包構(gòu)建一下這個(gè)命令
$ go build -o greet ./main.go
# 不帶任何參數(shù),使用默認(rèn)值
$ ./greet
Hello, world!
# 使用 --name 標(biāo)志
$ ./greet --name Gopher
Hello, Gopher!
# 使用別名 -n
$ ./greet -n 狗蛋
Hello, 狗蛋!命令和子命令
當(dāng)你的工具功能越來(lái)越復(fù)雜時(shí),就需要引入“命令” 和 “子命令”來(lái)組織功能了。這就像 git 有 commit、push、pull 等子命令一樣。我們來(lái)模擬一個(gè)簡(jiǎn)單的文件處理工具 filetool。
package main
import (
"fmt"
"log"
"os"
"github.com/urfave/cli/v2"
)
func main() {
app := &cli.App{
Name: "filetool",
Usage: "一個(gè)簡(jiǎn)單的文件處理工具",
Commands: []*cli.Command{
{
Name: "hash",
Aliases: []string{"h"},
Usage: "計(jì)算文件的哈希值",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "file",
Aliases: []string{"f"},
Usage: "指定輸入文件",
Required: true, // 這是一個(gè)必填項(xiàng)!
},
},
Action: func(c *cli.Context) error {
filePath := c.String("file")
// 這里的 hashFile 是我們自己實(shí)現(xiàn)的邏輯函數(shù)
fmt.Printf("正在為文件 '%s' 計(jì)算哈希...\n", filePath)
returnnil
},
},
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}上面是添加了命令,對(duì)于復(fù)雜的命令行程序,尤其是在業(yè)務(wù)系統(tǒng)里用作處理數(shù)據(jù)的命令行程序,往往還需要子命令的支持。這樣我們可以把處理一個(gè)大類數(shù)據(jù)的任務(wù)都劃分到同一個(gè)命令下,每個(gè)細(xì)分任務(wù)在寫成命令的子命令。
下面是一個(gè)添加子命令的簡(jiǎn)單例子:
var Word = &cli.Command{
Name: "word",
Aliases: []string{"w"},
Usage: "Word文檔處理相關(guān)命令",
Subcommands: []*cli.Command{
{
Name: "parse",
Usage: "解析Word文檔",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "input",
Aliases: []string{"i"},
Usage: "輸入文件路徑",
Required: true,
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "輸出文件路徑",
Required: true,
},
},
Action: func(c *cli.Context) error {
return logic.NewWordLogic(c.Context).ParseWord(c.String("input"), c.String("output"))
},
},
},
}我們把這個(gè)子命令加到上面的
func main() {
app := &cli.App{
Name: "filetool",
Usage: "一個(gè)簡(jiǎn)單的文件處理工具",
Commands: []*cli.Command{
// ......
word,
// 添加更多命令
},
}
// ......
}上面這個(gè)子命令的調(diào)用方式如下:
$ go build -o filetool ./main.go;
./filetool word parse -i input.docx -o output.txt最佳實(shí)踐
基礎(chǔ)用法已經(jīng)掌握了,但要構(gòu)建一個(gè)健壯、可維護(hù)的命令行工具,我們還需要借鑒一些真實(shí)項(xiàng)目中的經(jīng)驗(yàn)。下面這些技巧,能讓你的代碼質(zhì)量提升一個(gè)臺(tái)階。
鉤子函數(shù):用 Before和 After統(tǒng)一處理邏輯
你可能希望在每個(gè)命令執(zhí)行前后都做一些固定的操作,比如初始化日志、設(shè)置鏈路追蹤、上報(bào)監(jiān)控?cái)?shù)據(jù)或者記錄執(zhí)行時(shí)間等。urfave/cli 提供了 Before 和 After 鉤子函數(shù),來(lái)解決這個(gè)問(wèn)題。
下面是我的專欄項(xiàng)目使用 urfave/cli 時(shí)添加的鉤子:
func main() {
app := &cli.App{
Name: "gm-tools",
Usage: "Go Mall 工具集",
Before: func(c *cli.Context) error {
// 為每個(gè)命令創(chuàng)建帶有追蹤信息的上下文
ctx := context.Background()
spanId := util.GenerateSpanID(util.GetLocalIP())
ctx = context.WithValue(ctx, "spanid", spanId)
c.Context = ctx
cmdName := strings.Join(c.Args().Slice(), " ")
logger.Info(ctx, fmt.Sprintf("定時(shí)任務(wù)【%s】開(kāi)始執(zhí)行. 時(shí)間=【%s】)", cmdName, time.Now().Format(enum.TimeFormatHyphenedYMDHIS)))
returnnil
},
After: func(c *cli.Context) error {
// 記錄執(zhí)行的錯(cuò)誤
if c.Context.Err() != nil {
logger.Error(c.Context, "定時(shí)任務(wù)執(zhí)行失敗", c.Context.Err())
}
cmdName := strings.Join(c.Args().Slice(), " ")
logger.Info(c.Context, fmt.Sprintf("定時(shí)任務(wù)【%s】執(zhí)行完成. 時(shí)間=【%s", cmdName, time.Now().Format(enum.TimeFormatHyphenedYMDHIS)))
returnnil
},
Commands: []*cli.Command{
commands.Word,
// 添加更多工具命令
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}這樣無(wú)論你運(yùn)行哪個(gè)命令,Before 和 After 里的日志都會(huì)被打印出來(lái)。更重要的是,我們將一個(gè)帶有追蹤信息的 Go context.Context 注入到了 cli.Context 中,在后續(xù)的 Action 函數(shù)里,我們可以通過(guò) c.Context 取出這個(gè)上下文,并把它傳遞給業(yè)務(wù)邏輯,實(shí)現(xiàn)了全鏈路的追蹤!





























