首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一个库,用Go搞定命令行程序

一个库,用Go搞定命令行程序

作者头像
KevinYan
发布2025-09-02 10:36:13
发布2025-09-02 10:36:13
1790
举报
文章被收录于专栏:网管叨bi叨网管叨bi叨

嘿,大家好,我是网管。

我们平时做的Go项目除了写的各种API接口外,还经常会写任务脚本、命令行程序、定时任务等,其实这几个是一个东西,你写的任务脚本支持接受指令传参,那它不就是命令行程序了?再把程序部署到服务器用Go Cron加个任务就是定时任务了。

Go 官方有一个 flags 库提供了最基础的命令行参数支持,不过确实不好用,今天带你认识一个超赞的库——urfave/cli,它能让你用一种简单优雅的方式来构建命令行程序。

什么是urfave/cli?

urfave/cli 是一个用 Go 编写的、简单、快速且有趣的库,用于构建命令行应用程序。无论是小工具还是复杂的大型 CLI 程序,它都能轻松应对。它的设计哲学是让我们用声明式的方式来定义命令、子命令、标志(Flags),然后它会自动帮你处理参数解析、帮助文档生成等所有繁琐的工作,听起来是不是很棒?

安装

运行以下命令来安装 urfave/cli 的 v2 版本:

代码语言:javascript
复制
go get github.com/urfave/cli/v2

第一个 CLI 程序

我们从经典的 "Hello, World!" 开始,创建一个 main.go 文件,然后敲入以下代码:

代码语言:javascript
复制
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)
    }
}

运行命令程序

代码语言:javascript
复制
go run main.go

你会看到终端输出了 Hello, world!,当然我们也可以 build 后用真正的命令去运行

代码语言:javascript
复制
# build
go build -o greet ./main.go 
# 运行命令
./greet  

urfave/cli 自动为我们生成了帮助信息。上面这个命令运行时添加 --help 就能在控制台输出帮助信息。

添加命令行传参

只会说 "Hello, world!" 可不够,我们希望它能跟指定的人打招呼。这就要用到“标志”(Flags)了。

我们来修改一下代码,添加一个 --name的标志:

代码语言:javascript
复制
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)
    }
}

现在重新打包构建一下这个命令

代码语言:javascript
复制
$ go build -o greet ./main.go
# 不带任何参数,使用默认值
$ ./greet
Hello, world!

# 使用 --name 标志
$ ./greet --name Gopher
Hello, Gopher!

# 使用别名 -n
$ ./greet -n 狗蛋
Hello, 狗蛋!

命令和子命令

当你的工具功能越来越复杂时,就需要引入“命令” 和 “子命令”来组织功能了。这就像 git 有 commit、push、pull 等子命令一样。我们来模拟一个简单的文件处理工具 filetool。

代码语言:javascript
复制
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)
    }
}

上面是添加了命令,对于复杂的命令行程序,尤其是在业务系统里用作处理数据的命令行程序,往往还需要子命令的支持。这样我们可以把处理一个大类数据的任务都划分到同一个命令下,每个细分任务在写成命令的子命令。

下面是一个添加子命令的简单例子:

代码语言:javascript
复制
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"))
   },
  },
 },
}

我们把这个子命令加到上面的

代码语言:javascript
复制
func main() {
    app := &cli.App{
        Name:  "filetool",
        Usage: "一个简单的文件处理工具",
        Commands: []*cli.Command{
            // ......
            word,
            // 添加更多命令
        },
    }
    // ......
}

上面这个子命令的调用方式如下:

代码语言:javascript
复制
$ go build -o filetool ./main.go;
./filetool word parse -i input.docx -o output.txt

最佳实践

基础用法已经掌握了,但要构建一个健壮、可维护的命令行工具,我们还需要借鉴一些真实项目中的经验。下面这些技巧,能让你的代码质量提升一个台阶。

钩子函数:用 Before和 After统一处理逻辑

你可能希望在每个命令执行前后都做一些固定的操作,比如初始化日志、设置链路追踪、上报监控数据或者记录执行时间等。urfave/cli 提供了 Before 和 After 钩子函数,来解决这个问题。

下面是我的专栏项目使用 urfave/cli 时添加的钩子:

代码语言:javascript
复制
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 取出这个上下文,并把它传递给业务逻辑,实现了全链路的追踪!

所有代码在扫码加入我的专栏《Go项目整洁开发实战》后都能获取相应的实战案例。

代码模块化

当你的工具有几十个命令时,把所有定义都堆在 main.go 不是一个好的选择。正确的做法是,将每个主命令或一组相关的命令放到单独的文件或包里, 比如上一个例子中的命令注册方式,就是因为我把每个命令拆到了commands 目录的。

代码语言:javascript
复制
    Commands: []*cli.Command{
        commands.Word,
        // 添加更多工具命令
    },

这样也利于我们做职责单一的逻辑分层,比如下面是我付费专栏的项目中做的命令行程序模块的逻辑分层

总结

通过 urfave/cli 的声明式API,我们可以轻松定义命令、标志和层级关系。而结合 Before/After 钩子、代码模块化以及逻辑分层等最佳实践,即使是再复杂的CLI应用,我们也能游刃有余地进行开发和维护。

Go 社区还有一个 cobra 也是做命令行程序用的,但使用起来比我们今天介绍的 urfave/cli 更复杂一些。


结尾推荐一下我的专栏课程,专栏中除了今天介绍的命令行程序实战外,还会教你怎么用Go做好项目的开发和设计,搭建出一个实用、适合自己的Go项目的基础框架、怎么在写业务代码时做好项目的分层和解耦,欢迎扫下方二维码订阅专栏。

《Go项目搭建和整洁开发实战》专栏分为五大部分,重点章节如下

图片
图片
  • 第一部分介绍让框架变得好用的诸多实战技巧,比如通过自定义日志门面让项目日志更简单易用、支持自动记录请求的追踪信息和程序位置信息、通过自定义Error在实现Go error接口的同时支持给给错误添加错误链,方便追溯错误源头。
  • 第二部分:讲解项目分层架构的设计和划分业务模块的方法和标准,让你以后无论遇到什么项目都能按这套标准自己划分出模块和逻辑分层。后面几个部分均是该部分所讲内容的实践。
  • 第三部分:设计实现一个套支持多平台登录,Token泄露检测、同平台多设备登录互踢功能的用户认证体系,这套用户认证体系既可以在你未来开发产品时直接应用
  • 第四部分:商城app C端接口功能的实现,强化分层架构实现的讲解,这里还会讲解用责任链、策略和模版等设计模式去解决订单结算促销、支付方式支付场景等多种多样的实际问题。
  • 第五部分:单元测试、项目Docker镜像、K8s部署和服务保障相关的一些基础内容和注意事项。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 网管叨bi叨 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是urfave/cli?
  • 安装
  • 第一个 CLI 程序
  • 添加命令行传参
  • 命令和子命令
  • 最佳实践
    • 钩子函数:用 Before和 After统一处理逻辑
    • 代码模块化
    • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档