Heim  >  Artikel  >  Backend-Entwicklung  >  Technischer Deep Dive: Wie wir die Pizza-CLI mit Go und Cobra erstellt haben

Technischer Deep Dive: Wie wir die Pizza-CLI mit Go und Cobra erstellt haben

Linda Hamilton
Linda HamiltonOriginal
2024-09-24 06:18:35259Durchsuche

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

Letzte Woche veröffentlichte das OpenSauced-Engineering-Team die Pizza CLI, ein leistungsstarkes und zusammensetzbares Befehlszeilentool zum Generieren von CODEOWNER-Dateien und zur Integration in die OpenSauced-Plattform. Die Entwicklung robuster Befehlszeilentools mag einfach erscheinen, aber ohne sorgfältige Planung und durchdachte Paradigmen können CLIs schnell zu einem Code-Wirrwarr werden, der schwer zu warten und voller Fehler ist. In diesem Blog-Beitrag werden wir uns eingehend damit befassen, wie wir diese CLI mit Go erstellt haben, wie wir unsere Befehle mit Cobra organisieren und wie unser Lean-Engineering-Team schnell iteriert, um leistungsstarke Funktionen zu erstellen.

Mit Go und Cobra

Die Pizza CLI ist ein Go-Befehlszeilentool, das mehrere Standardbibliotheken nutzt. Die Einfachheit, Geschwindigkeit und der Fokus auf Systemprogrammierung machen Go zur idealen Wahl für die Erstellung von CLIs. Im Kern verwendet die Pizza-CLI spf13/cobra, eine CLI-Bootstrapping-Bibliothek in Go, um den gesamten Befehlsbaum zu organisieren und zu verwalten.

Sie können sich Cobra als das Gerüst vorstellen, das dafür sorgt, dass eine Befehlszeilenschnittstelle selbst funktioniert, die konsistente Funktion aller Flags ermöglicht und die Kommunikation mit Benutzern über Hilfenachrichten und automatisierte Dokumentation übernimmt.

Strukturierung der Codebasis

Eine der ersten (und größten) Herausforderungen beim Aufbau einer Cobra-basierten Go-CLI ist die Strukturierung Ihres gesamten Codes und Ihrer Dateien. Entgegen der landläufigen Meinung gibt es in Go keine vorgeschriebene Methode, dies zu tun. Weder der Befehl go build noch das Dienstprogramm gofmt werden sich darüber beschweren, wie Sie Ihre Pakete benennen oder Ihre Verzeichnisse organisieren. Dies ist einer der besten Aspekte von Go: Seine Einfachheit und Leistungsfähigkeit machen es einfach, Strukturen zu definieren, die für Sie und Ihr Ingenieurteam funktionieren!

Letztendlich ist es meiner Meinung nach am besten, sich eine Cobra-basierte Go-Codebasis als einen Befehlsbaum vorzustellen und zu strukturieren:

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

An der Basis des Baums befindet sich der Root-Befehl: Dies ist der Anker für Ihre gesamte CLI-Anwendung und erhält den Namen Ihrer CLI. Als untergeordnete Befehle angehängt, verfügen Sie über einen Baum mit Verzweigungslogik, der die Struktur Ihres gesamten CLI-Ablaufs bestimmt.

Eines der Dinge, die beim Erstellen von CLIs unglaublich leicht übersehen werden, ist die Benutzererfahrung. Normalerweise empfehle ich Leuten, beim Erstellen von Befehlen und untergeordneten Befehlsstrukturen einem „Wurzelverb-Substantiv“-Paradigma zu folgen, da es logisch abläuft und zu hervorragenden Benutzererlebnissen führt.

Zum Beispiel sehen Sie in Kubectl überall dieses Paradigma: „kubectl get pods“, „kubectl apply …“ oder „kubectl label pods …“ Dies gewährleistet einen sinnvollen Ablauf bei der Art und Weise, wie Benutzer mit Ihrer Befehlszeile interagieren Anwendung und hilft sehr, wenn man mit anderen Leuten über Befehle spricht.

Letztendlich können diese Struktur und dieser Vorschlag Aufschluss darüber geben, wie Sie Ihre Dateien und Verzeichnisse organisieren, aber letztendlich liegt es auch hier an Ihnen, zu bestimmen, wie Sie Ihre CLI strukturieren und den Ablauf den Endbenutzern präsentieren.

In der Pizza-CLI haben wir eine klar definierte Struktur, in der untergeordnete Befehle (und nachfolgende Enkel dieser untergeordneten Befehle) leben. Unter dem cmd-Verzeichnis in seinen eigenen Paketen erhält jeder Befehl seine eigene Implementierung. Das Root-Befehlsgerüst befindet sich in einem pkg/utils-Verzeichnis, da es sinnvoll ist, sich den Root-Befehl als ein Dienstprogramm der obersten Ebene vorzustellen, das von main.go verwendet wird, und nicht als einen Befehl, der möglicherweise viel Wartung erfordert. In der Regel werden Sie in Ihrer Root-Command-Go-Implementierung viele vorgefertigte Dinge einrichten, die Sie nicht oft anfassen, daher ist es schön, diese Dinge aus dem Weg zu räumen.

Hier ist eine vereinfachte Ansicht unserer Verzeichnisstruktur:

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

Diese Struktur ermöglicht eine klare Trennung der Anliegen und erleichtert die Pflege und Erweiterung der CLI, wenn sie wächst und wir weitere Befehle hinzufügen.

Go-git verwenden

Eine der Hauptbibliotheken, die wir in der Pizza-CLI verwenden, ist die Go-Git-Bibliothek, eine reine Git-Implementierung in Go, die hoch erweiterbar ist. Während der CODEOWNERS-Generierung ermöglicht uns diese Bibliothek, das Git-Ref-Protokoll zu iterieren, Codeunterschiede zu betrachten und zu bestimmen, welche Git-Autoren mit den von einem Benutzer definierten konfigurierten Attributionen verknüpft sind.

Das Iterieren des Git-Ref-Protokolls eines lokalen Git-Repos ist eigentlich ziemlich einfach:

// 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")
}

Wenn Sie eine Git-basierte Anwendung erstellen, empfehle ich auf jeden Fall die Verwendung von go-git: Es ist schnell, lässt sich gut in das Go-Ökosystem integrieren und kann für alle möglichen Dinge verwendet werden!

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!

Das obige ist der detaillierte Inhalt vonTechnischer Deep Dive: Wie wir die Pizza-CLI mit Go und Cobra erstellt haben. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn