为了账号安全,请及时绑定邮箱和手机立即绑定

技术深究:我们如何使用 Go 和 Cobra 构建 Pizza CLI

标签:
Go 运维工具

上周,OpenSauced 工程团队发布了 Pizza CLI,这是一个强大的可组合命令行工具,用于生成 CODEOWNER 文件并集成到 OpenSauced 平台。构建强大的命令行工具看似简单,但如果没有周密的规划和深思熟虑的设计,CLI 很快就会变成一团难以维护且充满 bug 的乱码。在这篇博客文章中,我们将深入探讨我们如何使用 Go 语言构建这个 CLI,如何使用 Cobra 组织我们的命令,以及我们的精简工程团队如何快速迭代以构建强大的功能。

使用 Go 和 Cobra

Pizza CLI 是一个使用多个标准库的 Go 语言命令行工具。Go 语言的简洁性、速度和系统编程特性使其成为构建 CLI 的理想选择。在核心部分,Pizza-CLI 使用 spf13/cobra,这是一个 Go 语言的 CLI 启动库,用于组织和管理整个命令树。

你可以把 Cobra 当作是构建命令行界面的基础框架,它使得所有标志能够一致地工作,并且负责通过帮助信息和自动化文档与用户进行沟通。

代码库的结构化

在使用 Cobra 构建 Go CLI 时,面临的第一个(也是最大的)挑战是如何组织所有的代码和文件。与普遍的看法相反,在 Go 中并没有规定的方式来完成这一点。无论是 go build 命令还是 gofmt 工具都不会抱怨你如何命名包或组织目录。这是 Go 的一个最好的方面:它的简单和强大使得定义适合你和你的工程团队的结构变得非常容易!

最终,依我看来,最好将基于 Cobra 的 Go 代码库视为一个命令树:

    ├── 根命令
    │   ├── 子命令
    │   ├── 子命令
    │   │   └── 孙命令

进入全屏模式 退出全屏模式

在树的底部是根命令:这是你整个 CLI 应用程序的锚点,并将获得你的 CLI 的名称。作为子命令附着,你将有一个分支逻辑树,它决定了你整个 CLI 流程的结构。

在构建 CLI 时,一个很容易被忽视的地方是用户体验。我通常建议人们在构建命令和子命令结构时遵循“根动词名词”的模式,因为它逻辑清晰,能带来很好的用户体验。

例如,在Kubectl中,你会看到这种模式随处可见:“kubectl get pods”,“kubectl apply …”,或“kubectl label pods …”。这确保了用户与命令行应用程序交互时的逻辑流程,并在与其他人员讨论命令时非常有帮助。

最终,这种结构和建议可以指导你如何组织文件和目录,但最终还是由你来决定如何构建你的 CLI 并向最终用户呈现流程。

在 Pizza CLI 中,我们有一个结构分明的设计,子命令(以及这些子命令的孙子命令)都居住在特定的位置。每个命令都在 cmd 目录下的独立包中拥有自己的实现。根命令的骨架存在于 pkg/utils 目录中,因为将根命令视为 main.go 使用的顶级工具比将其视为可能需要大量维护的命令更有用。通常,在你的根命令的 Go 实现中,你会有很多样板代码来设置一些内容,这些内容你不会经常改动,所以把这些内容处理好是很不错的。

这里是我们目录结构的简化视图:

    ├── main.go
    ├── pkg/
    │   ├── utils/
    │   │   └── root.go
    ├── cmd/
    │   ├── 子命令目录
    │   ├── 子命令目录
    │   │   └── 孙命令目录

进入全屏模式 退出全屏模式

这种结构允许清晰地分离关注点,使得在 CLI 增长并添加更多命令时更容易维护和扩展。

使用 go-git

我们在 Pizza-CLI 中使用的主要库之一是 go-git 库,这是一个高度可扩展的纯 Go 实现的 Git 库。在生成 CODEOWNERS 时,这个库使我们能够遍历 Git 引用日志,查看代码差异,并确定哪些 Git 作者与用户定义的配置属性相关联。

迭代本地 Git 仓库的 git ref 日志其实很简单:

    // 1. 打开本地的 Git 仓库
    repo, err := git.PlainOpen("/path/to/your/repo")
    if err != nil {
            panic("无法打开 Git 仓库")
    }

    // 2. 获取本地 Git 仓库的 HEAD 引用
    head, err := repo.Head()
    if err != nil {
            panic("无法获取仓库 HEAD")
    }

    // 3. 根据一些选项创建 Git ref 日志迭代器
    commitIter, err := repo.Log(&git.LogOptions{
            From:  head.Hash(),
    })
    if err != nil {
            panic("无法获取仓库日志迭代器")
    }

    defer commitIter.Close()

    // 4. 遍历提交历史
    err = commitIter.ForEach(func(commit *object.Commit) error {
            // 处理迭代器遍历的每个提交
            return nil
    })
    if err != nil {
            panic("无法处理提交迭代器")
    }

进入全屏模式 退出全屏模式

如果你正在构建一个基于 Git 的应用,我强烈推荐使用 go-git:它速度快,与 Go 生态系统集成良好,可以用来做各种事情!

集成 Posthog 监控

我们的工程和产品团队非常重视为最终用户提供最佳的命令行体验:这意味着我们已经采取措施集成匿名遥测,以便向 Posthog 报告野外的使用情况和错误。这使我们能够优先修复最重要的错误,快速迭代热门功能请求,并了解用户如何使用 CLI。

Posthog 提供了一个 官方的 Go 库,支持这种功能。首先,我们定义一个 Posthog 客户端:

    import "github.com/posthog/posthog-go"

    // PosthogCliClient 是围绕 posthog-go 客户端的包装器,并用作发送 OpenSauced CLI 命令遥测数据的 API 入口点
    type PosthogCliClient struct {
        // client 是 Posthog Go 客户端
        client posthog.Client

        // activated 表示用户是否启用了或禁用了遥测
        activated bool

        // uniqueID 是用户的唯一匿名标识符
        uniqueID string
    }

进入全屏模式 退出全屏模式

然后,在初始化一个新的客户端之后,我们可以通过我们定义的各种结构体方法来使用它。例如,在登录 OpenSauced 平台时,我们捕获登录成功的特定信息:

    // CaptureLogin 收集通过 CLI 登录 OpenSauced 的用户的相关遥测数据
    func (p *PosthogCliClient) CaptureLogin(username string) error {
        if p.activated {
            return p.client.Enqueue(posthog.Capture{
                DistinctId: username,
                Event:      "pizza_cli_user_logged_in",
            })
        }

        return nil
    }

进入全屏模式 退出全屏模式

在命令执行期间,各种“捕获”函数会被调用来捕获错误路径、正常路径等。

对于匿名化ID,我们使用Google优秀的UUID Go库

    newUUID := uuid.New().String()

进入全屏模式 退出全屏模式

这些 UUID 会存储在终端用户的本地机器上,格式为 JSON,位置在用户的主目录下的 ~/.pizza-cli/telemtry.json。这使得终端用户可以完全自主地删除此遥测数据(或者通过配置选项完全禁用遥测!),以确保在使用 CLI 时保持匿名。

迭代开发和测试

我们的精简工程团队遵循迭代开发流程,专注于快速交付小型、可测试的功能。通常,我们通过GitHub问题、拉取请求、里程碑和项目来实现这一点。我们广泛使用Go自带的测试框架,为单个函数编写单元测试,并为整个命令编写集成测试。

不幸的是,Go 的标准测试库默认情况下并没有很好的断言功能。使用“==”或其他操作符来比较是很简单的,但大多数时候,当你回头阅读测试代码时,能够通过断言如“assert.Equal”或“assert.Nil”来直观地了解发生了什么会更好。

我们集成了优秀的testify库及其“assert”功能,以实现更流畅的测试实施:

    config, _, err := LoadConfig(nonExistentPath)
    require.Error(t, err)
    assert.Nil(t, config)

进入全屏模式 退出全屏模式

使用 Just

我们在 OpenSauced 中广泛使用 Just,这是一个命令运行工具,类似于 GNU 的 “make”,用于轻松执行小型脚本。这使我们能够快速让新团队成员或社区成员融入我们的 Go 生态系统,因为构建和测试只需简单地执行 “just build” 或 “just test” 即可!

例如,要在 Just 中创建一个简单的构建工具,在 justfile 中我们可以有:

    build:
      go build main.go -o build/pizza

进入全屏模式 退出全屏模式

这将会把 Go 二进制文件构建到 build/ 目录中。现在,本地构建只需执行一个“just”命令即可。

但我们已经能够通过使用Just整合更多的功能,并将其作为我们整个构建、测试和开发框架执行的核心。例如,要为本地架构构建一个带有构建时间变量(如二进制文件构建所针对的sha、版本、日期时间等)的二进制文件,我们可以使用本地环境并在脚本执行“go build”之前运行额外的步骤:

    build:
        #!/usr/bin/env sh
      echo "为本地架构构建"

      export VERSION="${RELEASE_TAG_VERSION:-dev}"
      export DATETIME=$(date -u +"%Y-%m-%d-%H:%M:%S")
      export SHA=$(git rev-parse HEAD)

      go build \
        -ldflags="-s -w \
        -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \
        -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \
        -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \
        -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \
        -o build/pizza

进入全屏模式 退出全屏模式

我们甚至扩展了这一功能,以支持跨架构和操作系统的构建:Go 使用 GOARCH 和 GOOS 环境变量来确定要为目标 CPU 架构和操作系统构建。为了构建其他变体,我们可以为这些变体创建特定的 Just 命令:

    # 针对 Darwin (即 MacOS) 的 arm64 架构 (即 Apple Silicon) 构建
    build-darwin-arm64:
      #!/usr/bin/env sh

      echo "正在构建 darwin arm64"

      export VERSION="${RELEASE_TAG_VERSION:-dev}"
      export DATETIME=$(date -u +"%Y-%m-%d-%H:%M:%S")
      export SHA=$(git rev-parse HEAD)
      export CGO_ENABLED=0
      export GOOS="darwin"
      export GOARCH="arm64"

      go build \
        -ldflags="-s -w \
        -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \
        -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \
        -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \
        -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \
        -o build/pizza-${GOOS}-${GOARCH}

进入全屏模式 退出全屏模式

结论

使用 Go 和 Cobra 构建 Pizza CLI 的过程令人兴奋,我们非常高兴与您分享。Go 的高性能和简洁性结合 Cobra 的强大命令结构,使我们能够创建一个不仅强大且健壮,而且用户友好且易于维护的工具。

我们邀请您探索 Pizza CLI GitHub 仓库,尝试使用该工具,并告诉我们您的想法。您的反馈和贡献对于我们帮助开发团队更轻松地管理代码所有权至关重要!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消