„Wenn ein Arbeiter seine Arbeit gut machen will, muss er zuerst seine Werkzeuge schärfen.“ – Konfuzius, „Die Gespräche des Konfuzius. Lu Linggong“
Titelseite > Programmierung > 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

Veröffentlicht am 01.11.2024
Durchsuche:451

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 Engineering-Team 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, bei der Erstellung 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 Go-Implementierung mit dem Root-Befehl eine Menge vorgefertigter 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 Wartung 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 Attributen 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!

Integration der Posthog-Telemetrie

Unser Technik- und Produktteam ist stark daran interessiert, unseren Endbenutzern das bestmögliche Befehlszeilenerlebnis zu bieten: Das bedeutet, dass wir Schritte unternommen haben, um anonymisierte Telemetrie zu integrieren, die Posthog über Nutzung und Fehler im Freien berichten kann. Dies hat es uns ermöglicht, die wichtigsten Fehler zuerst zu beheben, schnell auf beliebte Funktionsanfragen zu reagieren und zu verstehen, wie unsere Benutzer die CLI verwenden.

Posthog verfügt über eine Erstanbieter-Bibliothek in Go, die genau diese Funktionalität unterstützt. Zuerst definieren wir einen 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
}

Nachdem wir einen neuen Client initialisiert haben, können wir ihn über die verschiedenen von uns definierten Strukturmethoden verwenden. Wenn wir uns beispielsweise bei der OpenSauced-Plattform anmelden, erfassen wir spezifische Informationen über eine erfolgreiche Anmeldung:

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

Während der Befehlsausführung werden die verschiedenen „Capture“-Funktionen aufgerufen, um Fehlerpfade, glückliche Pfade usw. zu erfassen.

Für die anonymisierten IDs verwenden wir die hervorragende UUID Go-Bibliothek von Google:

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

Diese UUIDs werden lokal auf den Computern der Endbenutzer als JSON in ihrem Home-Verzeichnis gespeichert: ~/.pizza-cli/telemtry.json. Dies gibt dem Endbenutzer die vollständige Befugnis und Autonomie, diese Telemetriedaten bei Bedarf zu löschen (oder die Telemetrie über Konfigurationsoptionen ganz zu deaktivieren!), um sicherzustellen, dass er bei der Verwendung der CLI anonym bleibt.

Iterative Entwicklung und Tests

Unser Lean-Engineering-Team folgt einem iterativen Entwicklungsprozess und konzentriert sich auf die schnelle Bereitstellung kleiner, testbarer Funktionen. Normalerweise tun wir dies über GitHub-Issues, Pull Requests, Meilensteine ​​und Projekte. Wir nutzen das integrierte Test-Framework von Go ausgiebig und schreiben Unit-Tests für einzelne Funktionen und Integrationstests für ganze Befehle.

Leider verfügt die Standardtestbibliothek von Go nicht sofort über eine hervorragende Assertionsfunktionalität. Es ist einfach genug, „==“ oder andere Operanden zu verwenden, aber meistens ist es beim Zurückgehen und Durchlesen von Tests hilfreich, einen Blick darauf werfen zu können, was mit Behauptungen wie „assert.Equal“ oder „assert.Nil“ los ist ”.

Wir haben die hervorragende Testify-Bibliothek mit ihrer „Assert“-Funktionalität integriert, um eine reibungslosere Testimplementierung zu ermöglichen:

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

Mit Just

Wir verwenden bei OpenSauced häufig Just, ein Befehls-Runner-Dienstprogramm, ähnlich wie GNUs „make“, zum einfachen Ausführen kleiner Skripte. Dies hat es uns ermöglicht, schnell neue Teammitglieder oder Community-Mitglieder in unser Go-Ökosystem einzubinden, da das Erstellen und Testen so einfach ist wie „einfach erstellen“ oder „einfach testen“!

Um beispielsweise ein einfaches Build-Dienstprogramm in Just zu erstellen, können wir in einer Justfile Folgendes haben:

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

Dadurch wird eine Go-Binärdatei im Verzeichnis build/ erstellt. Jetzt ist das lokale Erstellen so einfach wie das Ausführen eines „einfachen“ Befehls.

Aber wir konnten mehr Funktionalität in die Verwendung von Just integrieren und haben es zu einem Eckpfeiler für die Ausführung unseres gesamten Build-, Test- und Entwicklungs-Frameworks gemacht. Um beispielsweise eine Binärdatei für die lokale Architektur mit injizierten Buildzeitvariablen (wie dem SHA, gegen den die Binärdatei erstellt wurde, der Version, dem Datum und der Uhrzeit usw.) zu erstellen, können wir die lokale Umgebung verwenden und zusätzliche Schritte im Skript ausführen bevor Sie den „go build“ ausführen:

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

Wir haben dies sogar erweitert, um architektur- und betriebssystemübergreifende Builds zu ermöglichen: Go verwendet die Umgebungsvariablen GOARCH und GOOS, um zu wissen, auf welcher CPU-Architektur und auf welchem ​​Betriebssystem gebaut werden soll. Um andere Varianten zu erstellen, können wir dafür spezielle Just-Befehle erstellen:

# 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}

Abschluss

Der Aufbau der Pizza-CLI mit Go und Cobra war eine aufregende Reise und wir freuen uns, sie mit Ihnen zu teilen. Durch die Kombination der Leistung und Einfachheit von Go mit der leistungsstarken Befehlsstrukturierung von Cobra konnten wir ein Tool erstellen, das nicht nur robust und leistungsstark, sondern auch benutzerfreundlich und wartbar ist.

Wir laden Sie ein, das GitHub-Repository von Pizza CLI zu erkunden, das Tool auszuprobieren und uns Ihre Gedanken mitzuteilen. Ihr Feedback und Ihre Beiträge sind von unschätzbarem Wert, da wir daran arbeiten, die Code-Ownership-Verwaltung für Entwicklungsteams überall einfacher zu machen!

Freigabeerklärung Dieser Artikel ist abgedruckt unter: https://dev.to/opensauced/technical-deep-dive-how-we-built-the-pizza-cli-using-go-and-cobra-oad?1 Falls ein Verstoß vorliegt Bitte kontaktieren Sie Study_golang @163.comdelete
Neuestes Tutorial Mehr>

Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.

Copyright© 2022 湘ICP备2022001581号-3