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





























