首頁  >  文章  >  後端開發  >  技術深入探討:我們如何使用 Go 和 Cobra 來建立 Pizza CLI

技術深入探討:我們如何使用 Go 和 Cobra 來建立 Pizza CLI

Linda Hamilton
Linda Hamilton原創
2024-09-24 06:18:35105瀏覽

Technical Deep Dive: How We Built the Pizza CLI Using Go and Cobra

上週,OpenSauced 工程團隊發布了 Pizza CLI,這是一個功能強大且可組合的命令列工具,用於生成 CODEOWNER 檔案並與 OpenSauced 平台整合。建立強大的命令列工具看起來很簡單,但如果沒有仔細的規劃和深思熟慮的範例,CLI 很快就會變成難以維護且充滿錯誤的混亂程式碼。在這篇文章中,我們將深入探討如何使用 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 程式碼庫視為命令樹:

├── Root command
│   ├── Child command
│   ├── Child command
│   │   └── Grandchild command

樹的底部是根命令:這是整個 CLI 應用程式的錨點,並將取得 CLI 的名稱。作為子命令附加,您將擁有一個分支邏輯樹,它告知整個 CLI 流程如何運作的結構。

建構 CLI 時最容易忽略的事情之一就是使用者體驗。我通常建議人們在建立命令和子命令結構時遵循「根動詞名詞」範例,因為它邏輯流暢並帶來出色的使用者體驗。

例如,在Kubectl 中,您會隨處看到這種範例:「kubectl get pods」、「kubectl apply …」或「kubectl label pods …」這確保了使用者如何與命令列互動的合理流程應用程式,並且在與其他人談論命令時有很大幫助。

最後,此結構和建議可以告訴您如何組織文件和目錄,但最終還是由您決定如何建立 CLI 並將流程呈現給最終用戶。

在 Pizza CLI 中,我們有一個定義良好的結構,子命令(以及這些子命令的後續子命令)所在的結構。在各自套件的 cmd 目錄下,每個指令都有自己的實作。 root 命令腳手架存在於 pkg/utils 目錄中,因為將 root 命令視為 main.go 使用的頂級實用程式非常有用,而不是可能需要大量維護的命令。通常,在你的根命令 Go 實作中,你會有很多樣板設定你不會太多接觸的東西,所以最好把這些東西去掉。

這是我們目錄結構的簡化視圖:

├── main.go
├── pkg/
│   ├── utils/
│   │   └── root.go
├── cmd/
│   ├── Child command dir
│   ├── Child command dir
│   │   └── Grandchild command dir

這種結構可以清晰地分離關注點,並且隨著 CLI 的增長以及我們添加更多命令,可以更輕鬆地維護和擴展 CLI。

使用 go-git

我們在 Pizza-CLI 中使用的主要函式庫之一是 go-git 函式庫,它是 Go 中的一個純 git 實現,具有高度可擴展性。在 CODEOWNERS 生成過程中,該程式庫使我們能夠迭代 git ref 日誌、查看程式碼差異並確定哪些 git 作者與使用者定義的配置屬性相關聯。

迭代本機 git 儲存庫的 git ref 日誌其實非常簡單:

// 1. Open the local git repository
repo, err := git.PlainOpen("/path/to/your/repo")
if err != nil {
        panic("could not open git repository")
}

// 2. Get the HEAD reference for the local git repo
head, err := repo.Head()
if err != nil {
        panic("could not get repo head")
}

// 3. Create a git ref log iterator based on some options
commitIter, err := repo.Log(&git.LogOptions{
        From:  head.Hash(),
})
if err != nil {
        panic("could not get repo log iterator")
}

defer commitIter.Close()

// 4. Iterate through the commit history
err = commitIter.ForEach(func(commit *object.Commit) error {
        // process each commit as the iterator iterates them
        return nil
})
if err != nil {
        panic("could not process commit iterator")
}

如果您正在建立基於 Git 的應用程序,我絕對推薦使用 go-git:它速度快,與 Go 生態系統整合良好,並且可用於執行各種操作!

Integrating Posthog telemetry

Our engineering and product team is deeply invested in bringing the best possible command line experience to our end users: this means we’ve taken steps to integrate anonymized telemetry that can report to Posthog on usage and errors out in the wild. This has allowed us to fix the most important bugs first, iterate quickly on popular feature requests, and understand how our users are using the CLI.

Posthog has a first party library in Go that supports this exact functionality. First, we define a Posthog client:

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

// PosthogCliClient is a wrapper around the posthog-go client and is used as a
// API entrypoint for sending OpenSauced telemetry data for CLI commands
type PosthogCliClient struct {
    // client is the Posthog Go client
    client posthog.Client

    // activated denotes if the user has enabled or disabled telemetry
    activated bool

    // uniqueID is the user's unique, anonymous identifier
    uniqueID string
}

Then, after initializing a new client, we can use it through the various struct methods we’ve defined. For example, when logging into the OpenSauced platform, we capture specific information on a successful login:

// CaptureLogin gathers telemetry on users who log into OpenSauced via the CLI
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
}

During command execution, the various “capture” functions get called to capture error paths, happy paths, etc.

For the anonymized IDs, we use Google’s excellent UUID Go library:

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

These UUIDs get stored locally on end users machines as JSON under their home directory: ~/.pizza-cli/telemtry.json. This gives the end user complete authority and autonomy to delete this telemetry data if they want (or disable telemetry altogether through configuration options!) to ensure they’re staying anonymous when using the CLI.

Iterative Development and Testing

Our lean engineering team follows an iterative development process, focusing on delivering small, testable features rapidly. Typically, we do this through GitHub issues, pull requests, milestones, and projects. We use Go's built-in testing framework extensively, writing unit tests for individual functions and integration tests for entire commands.

Unfortunately, Go’s standard testing library doesn’t have great assertion functionality out of the box. It’s easy enough to use “==” or other operands, but most of the time, when going back and reading through tests, it’s nice to be able to eyeball what’s going on with assertions like “assert.Equal” or “assert.Nil”.

We’ve integrated the excellent testify library with its “assert” functionality to allow for smoother test implementation:

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

Using Just

We heavily use Just at OpenSauced, a command runner utility, much like GNU’s “make”, for easily executing small scripts. This has enabled us to quickly onramp new team members or community members to our Go ecosystem since building and testing is as simple as “just build” or “just test”!

For example, to create a simple build utility in Just, within a justfile, we can have:

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

Which will build a Go binary into the build/ directory. Now, building locally is as simple as executing a “just” command.

But we’ve been able to integrate more functionality into using Just and have made it a cornerstone of how our entire build, test, and development framework is executed. For example, to build a binary for the local architecture with injected build time variables (like the sha the binary was built against, the version, the date time, etc.), we can use the local environment and run extra steps in the script before executing the “go build”:

build:
    #!/usr/bin/env sh
  echo "Building for local arch"

  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

We’ve even extended this to enable cross architecture and OS build: Go uses the GOARCH and GOOS env vars to know which CPU architecture and operating system to build against. To build other variants, we can create specific Just commands for that:

# Builds for Darwin linux (i.e., MacOS) on arm64 architecture (i.e. Apple silicon)
build-darwin-arm64:
  #!/usr/bin/env sh

  echo "Building 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}

Conclusion

Building the Pizza CLI using Go and Cobra has been an exciting journey and we’re thrilled to share it with you. The combination of Go's performance and simplicity with Cobra's powerful command structuring has allowed us to create a tool that's not only robust and powerful, but also user-friendly and maintainable.

We invite you to explore the Pizza CLI GitHub repository, try out the tool, and let us know your thoughts. Your feedback and contributions are invaluable as we work to make code ownership management easier for development teams everywhere!

以上是技術深入探討:我們如何使用 Go 和 Cobra 來建立 Pizza CLI的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn