ホームページ  >  記事  >  バックエンド開発  >  技術的な詳細: 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 エンジニアリング チームは、CODEOWNER ファイルを生成し、OpenSauced プラットフォームと統合するための強力で構成可能なコマンドライン ツールである Pizza CLI をリリースしました。堅牢なコマンドライン ツールの構築は簡単そうに見えますが、慎重な計画と思慮深いパラダイムがなければ、CLI はすぐに保守が困難でバグだらけの複雑なコードになってしまう可能性があります。このブログ投稿では、Go を使用してこの CLI を構築した方法、Cobra を使用してコマンドを編成した方法、そして無駄のないエンジニアリング チームが強力な機能を構築するためにどのように迅速に反復したかについて詳しく説明します。

Go と Cobra の使用

Pizza CLI は、いくつかの標準ライブラリを利用する Go コマンドライン ツールです。 Go はシンプルさ、スピード、システム プログラミングに重点を置いているため、CLI を構築するのに理想的な選択肢です。 Pizza-CLI は、その中核として、Go の CLI ブートストラップ ライブラリである spf13/cobra を使用して、コマンド ツリー全体を編成および管理します。

Cobra は、コマンドライン インターフェイス自体を機能させ、すべてのフラグが一貫して機能できるようにし、ヘルプ メッセージや自動ドキュメントを介してユーザーとのコミュニケーションを処理する足場と考えることができます。

コードベースの構造化

Cobra ベースの Go CLI を構築する際の最初の (そして最大の) 課題の 1 つは、すべてのコードとファイルをどのように構成するかです。一般的な考えに反して、Go ではこれを行うための規定された方法はありません。 go build コマンドも gofmt ユーティリティも、パッケージの名前の付け方やディレクトリの編成方法については問題を出しません。これは Go の最も優れた部分の 1 つです。そのシンプルさと強力さにより、あなたとあなたのエンジニアリング チームに適した構造を簡単に定義できます!

最終的には、私の意見では、Cobra ベースの Go コードベースをコマンドのツリーとして考えて構造化するのが最善です。

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

ツリーのベースにはルート コマンドがあります。これは CLI アプリケーション全体のアンカーであり、CLI の名前を取得します。子コマンドとしてアタッチすると、CLI フロー全体がどのように機能するかの構造を伝える分岐ロジックのツリーが作成されます。

CLI を構築するときに非常に見落としがちなことの 1 つは、ユーザー エクスペリエンスです。コマンドや子コマンド構造を構築するときは、論理的に流れ、優れたユーザー エクスペリエンスにつながるため、通常、「ルート動詞名詞」パラダイムに従うことをお勧めします。

たとえば、Kubectl では、「kubectl get pods」、「kubectl apply …」、または「kubectl label pods …」というパラダイムが随所で見られます。これにより、ユーザーがコマンド ラインを操作する方法についての合理的なフローが保証されます。このアプリケーションは、他の人とコマンドについて話すときに非常に役立ちます。

最終的に、この構造と提案は、ファイルとディレクトリをどのように整理するかを示すことができますが、繰り返しになりますが、CLI をどのように構造化し、エンドユーザーにフローを提示するかを決定するのは最終的にはあなた次第です。

Pizza CLI には、子コマンド (およびそれらの子コマンドの後続の孫) が存在する、明確に定義された構造があります。独自のパッケージの cmd ディレクトリの下で、各コマンドは独自の実装を取得します。 root コマンドのスキャフォールディングは pkg/utils ディレクトリに存在します。これは、root コマンドを、多くのメンテナンスが必要になる可能性のあるコマンドではなく、main.go によって使用されるトップレベルのユーティリティとして考えると便利だからです。通常、root コマンドの Go 実装では、あまり触れない定型的な設定がたくさんあるため、それらを邪魔にならないようにしておくと便利です。

ディレクトリ構造を簡略化して示します:

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

この構造により、懸念事項を明確に分離できるため、CLI の成長やコマンドの追加に応じて、CLI の保守と拡張が容易になります。

go-gitの使用

Pizza-CLI で使用する主要なライブラリの 1 つは、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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。