>  기사  >  백엔드 개발  >  기술 심층 분석: Go 및 Cobra를 사용하여 Pizza CLI를 구축한 방법

기술 심층 분석: Go 및 Cobra를 사용하여 Pizza CLI를 구축한 방법

Linda Hamilton
Linda Hamilton원래의
2024-09-24 06:18:35257검색

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를 구축할 때 가장 먼저(그리고 가장 큰) 과제 중 하나는 모든 코드와 파일을 구조화하는 방법입니다. 대중적인 믿음과는 달리, Go에는 이를 수행하는 규정된 방법이 없습니다. go build 명령이나 gofmt 유틸리티는 패키지 이름 지정이나 디렉터리 구성 방법에 대해 불평하지 않습니다. 이것이 Go의 가장 좋은 부분 중 하나입니다. 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 디렉터리 아래에서 각 명령은 자체적으로 구현됩니다. 루트 명령 스캐폴딩은 pkg/utils 디렉토리에 존재합니다. 왜냐하면 루트 명령을 유지 관리가 많이 필요한 명령이 아니라 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으로 문의하세요.