Tworzenie aplikacji CLI w Go z użyciem Cobra i Viper

Rozwój CLI w Go z użyciem frameworków Cobra i Viper

Page content

Interfejs wiersza poleceń (CLI) to istotne narzędzia dla programistów, administratorów systemów oraz specjalistów DevOps.

Dwie biblioteki w języku Go stały się standardem dla rozwoju CLI w Go: Cobra do struktury poleceń i Viper do zarządzania konfiguracją.

Język Go zaczął się wyróżniać jako doskonały język do tworzenia narzędzi CLI dzięki swojej wydajności, prostemu wdrażaniu oraz wsparciu dla wielu platform.

Tetris

Dlaczego warto wybrać Go do aplikacji CLI

Go oferuje przekonujące zalety dla rozwoju aplikacji CLI:

  • Dystrybucja pojedynczego pliku: Nie wymaga zależności uruchomieniowych ani menedżerów pakietów
  • Szybkie wykonanie: Kompilacja natywna zapewnia bardzo dobre wydajność
  • Wsparcie wieloplatformowe: Łatwe kompilowanie dla Linux, macOS, Windows i wielu innych
  • Silna biblioteka standardowa: Bogate narzędzia do operacji plików, sieci i przetwarzania tekstu
  • Wątkowość: Wbudowane goroutines do operacji równoległych
  • Typowanie statyczne: Wykrywanie błędów w czasie kompilacji

Popularne narzędzia CLI zbudowane w Go obejmują Docker, Kubernetes (kubectl), Hugo, Terraform i GitHub CLI. Jeśli jesteś nowy w Go lub potrzebujesz szybkiego odniesienia, sprawdź nasz Arkusz wskazówek dla Go z istotnymi składnią i wzorcami Go.

Wprowadzenie do Cobra

Cobra to biblioteka oferująca prosty interfejs do tworzenia potężnych, nowoczesnych aplikacji CLI. Stworzona przez Steve’a Francia (spf13), tego samego autora, który tworzył Hugo i Viper, Cobra jest używana w wielu najpopularniejszych projektach w Go.

Kluczowe cechy Cobra

Struktura poleceń: Cobra implementuje wzorzec komend, umożliwiając tworzenie aplikacji z komendami i podkomendami (np. git commit lub docker run).

Obsługa flag: Lokalne i trwałe flagi z automatycznym parsowaniem i konwersją typów.

Automatyczna pomoc: Generuje tekst pomocy i informacje o użyciu automatycznie.

Inteligentne sugestie: Udostępnia sugestie, gdy użytkownicy popełniają błędy ortograficzne (“Czy chodziło o ‘status’?”).

Kompletacje powłoki: Generuje skrypty kompletacji dla bash, zsh, fish i PowerShell.

Elastyczne wyjścia: Działa płynnie z niestandardowymi formatami i stylami wyjścia.

Wprowadzenie do Viper

Viper to kompletna propozycja konfiguracji dla aplikacji w Go, zaprojektowana do płynnego działania z Cobra. Obsługuje konfigurację z wielu źródeł z wyraźnym porządkiem priorytetów.

Kluczowe cechy Viper

Wiele źródeł konfiguracji:

  • Pliki konfiguracyjne (JSON, YAML, TOML, HCL, INI, envfile, Java properties)
  • Zmienne środowiskowe
  • Flagi wiersza poleceń
  • Systemy konfiguracji zdalnej (etcd, Consul)
  • Wartości domyślne

Porządek priorytetów konfiguracji:

  1. Jawne wywołania Set
  2. Flagi wiersza poleceń
  3. Zmienne środowiskowe
  4. Plik konfiguracyjny
  5. Magazyn klucz/wartość
  6. Wartości domyślne

Monitorowanie w czasie rzeczywistym: Monitoruje pliki konfiguracyjne i automatycznie odświeża je w przypadku zmian.

Konwersja typów: Automatyczna konwersja do różnych typów Go (ciąg, liczba całkowita, wartość logiczna, czas itp.).

Rozpoczynanie: Instalacja

Najpierw zainicjalizuj nowy moduł Go i zainstaluj obie biblioteki:

go mod init myapp
go get -u github.com/spf13/cobra@latest
go get -u github.com/spf13/viper@latest

Opcjonalnie, zainstaluj generator CLI Cobra do tworzenia szkieletów:

go install github.com/spf13/cobra-cli@latest

Budowanie pierwszej aplikacji CLI

Zbudujmy praktyczny przykład: narzędzie CLI do zarządzania zadaniami z obsługą konfiguracji.

Struktura projektu

mytasks/
├── cmd/
│   ├── root.go
│   ├── add.go
│   ├── list.go
│   └── complete.go
├── config/
│   └── config.go
├── main.go
└── config.yaml

Główny punkt wejścia

// main.go
package main

import "mytasks/cmd"

func main() {
    cmd.Execute()
}

Polecenie główne z integracją Viper

// cmd/root.go
package cmd

import (
    "fmt"
    "os"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var cfgFile string

var rootCmd = &cobra.Command{
    Use:   "mytasks",
    Short: "Prosty kli do zarządzania zadaniami",
    Long: `MyTasks to kli do zarządzania zadaniami, który pomaga w organizowaniu 
codziennych zadań. Stworzony z użyciem Cobra i Viper.`,
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func init() {
    cobra.OnInitialize(initConfig)
    
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", 
        "plik konfiguracyjny (domyślnie $HOME/.mytasks.yaml)")
    rootCmd.PersistentFlags().String("db", "", 
        "ścieżka do pliku bazy danych")
    
    viper.BindPFlag("database", rootCmd.PersistentFlags().Lookup("db"))
}

func initConfig() {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        home, err := os.UserHomeDir()
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
        
        viper.AddConfigPath(home)
        viper.AddConfigPath(".")
        viper.SetConfigType("yaml")
        viper.SetConfigName(".mytasks")
    }
    
    viper.SetEnvPrefix("MYTASKS")
    viper.AutomaticEnv()
    
    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Używany plik konfiguracyjny:", viper.ConfigFileUsed())
    }
}

Dodawanie podkomend

// cmd/add.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var priority string

var addCmd = &cobra.Command{
    Use:   "add [opis zadania]",
    Short: "Dodaj nowe zadanie",
    Args:  cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        task := args[0]
        db := viper.GetString("database")
        
        fmt.Printf("Dodawanie zadania: %s\n", task)
        fmt.Printf("Priorytet: %s\n", priority)
        fmt.Printf("Baza danych: %s\n", db)
        
        // Tutaj zaimplementujesz rzeczywiste przechowywanie zadań
    },
}

func init() {
    rootCmd.AddCommand(addCmd)
    
    addCmd.Flags().StringVarP(&priority, "priority", "p", "medium", 
        "Priorytet zadania (niski, średni, wysoki)")
}

Polecenie listy

// cmd/list.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var showCompleted bool

var listCmd = &cobra.Command{
    Use:   "list",
    Short: "Wyświetl wszystkie zadania",
    Run: func(cmd *cobra.Command, args []string) {
        db := viper.GetString("database")
        
        fmt.Printf("Wyświetlanie zadań z: %s\n", db)
        fmt.Printf("Pokaż ukończone: %v\n", showCompleted)
        
        // Tutaj zaimplementujesz rzeczywiste wyświetlanie zadań
    },
}

func init() {
    rootCmd.AddCommand(listCmd)
    
    listCmd.Flags().BoolVarP(&showCompleted, "completed", "c", false, 
        "Pokaż ukończone zadania")
}

Implementacja przechowywania danych

Dla produkcyjnego narzędzia do zarządzania zadaniami CLI, musisz zaimplementować rzeczywiste przechowywanie danych. Choć tutaj używamy prostego konfiguracji ścieżki bazy danych, masz kilka opcji przechowywania danych:

  • SQLite: Lekki, bezserwerowy baza danych idealna do narzędzi CLI
  • PostgreSQL/MySQL: Pełnoprawne bazy danych dla bardziej złożonych aplikacji
  • Pliki JSON/YAML: Proste przechowywanie danych w plikach dla potrzeb lekkich
  • Bazy danych wbudowane: BoltDB, BadgerDB do przechowywania klucz/wartość

Jeśli pracujesz z relacyjnymi bazami danych takimi jak PostgreSQL, chcesz użyć ORM lub budownika zapytań. Dla kompletnego porównania bibliotek baz danych w Go, zobacz nasz przewodnik po Porównaniu ORM dla PostgreSQL w Go: GORM vs Ent vs Bun vs sqlc.

Zaawansowana konfiguracja z Viper

Przykład pliku konfiguracyjnego

# .mytasks.yaml
database: ~/.mytasks.db
format: table
colors: true

notifications:
  enabled: true
  sound: true

priorities:
  high: red
  medium: yellow
  low: green

Odczytywanie zagnieżdżonej konfiguracji

func getNotificationSettings() {
    enabled := viper.GetBool("notifications.enabled")
    sound := viper.GetBool("notifications.sound")
    
    fmt.Printf("Powiadomienia włączone: %v\n", enabled)
    fmt.Printf("Dźwięk włączony: %v\n", sound)
}

func getPriorityColors() map[string]string {
    return viper.GetStringMapString("priorities")
}

Zmienne środowiskowe

Viper automatycznie odczytuje zmienne środowiskowe z konfigurowanym prefiksem:

export MYTASKS_DATABASE=/tmp/tasks.db
export MYTASKS_NOTIFICATIONS_ENABLED=false
mytasks list

Odświeżanie konfiguracji w czasie rzeczywistym

func watchConfig() {
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        fmt.Println("Zmieniono plik konfiguracyjny:", e.Name)
        // Odśwież komponenty zależne od konfiguracji
    })
}

Najlepsze praktyki

1. Użyj generatora Cobra dla spójności

cobra-cli init
cobra-cli add serve
cobra-cli add create

2. Organizuj polecenia w osobnych plikach

Zachowuj każde polecenie w osobnym pliku w katalogu cmd/ dla łatwiejszego utrzymania.

3. Wykorzystaj trwałe flagi

Używaj trwałych flag dla opcji, które mają zastosowanie do wszystkich podkomend:

rootCmd.PersistentFlags().StringP("output", "o", "text", 
    "Format wyjścia (tekst, json, yaml)")

4. Zaimplementuj odpowiednie obsługę błędów

var rootCmd = &cobra.Command{
    Use:   "myapp",
    Short: "Moja aplikacja",
    RunE: func(cmd *cobra.Command, args []string) error {
        if err := doSomething(); err != nil {
            return fmt.Errorf("nie udało się wykonać czegoś: %w", err)
        }
        return nil
    },
}

5. Udostępnij znaczący tekst pomocy

var cmd = &cobra.Command{
    Use:   "deploy [environment]",
    Short: "Wdrażanie aplikacji w określonym środowisku",
    Long: `Wdraża budowanie i wdrażanie aplikacji w określonym 
środowisku. Obsługiwane środowiska to:
  - rozwijanie (dev)
  - testowanie (staging)
  - produkcja (prod)`,
    Example: `  myapp deploy staging
  myapp deploy production --version=1.2.3`,
}

6. Ustaw rozsądne domyślne wartości

func init() {
    viper.SetDefault("port", 8080)
    viper.SetDefault("timeout", "30s")
    viper.SetDefault("retries", 3)
}

7. Walidacja konfiguracji

func validateConfig() error {
    port := viper.GetInt("port")
    if port < 1024 || port > 65535 {
        return fmt.Errorf("nieprawidłowy port: %d", port)
    }
    return nil
}

Testowanie aplikacji CLI

Testowanie poleceń

// cmd/root_test.go
package cmd

import (
    "bytes"
    "testing"
)

func TestRootCommand(t *testing.T) {
    cmd := rootCmd
    b := bytes.NewBufferString("")
    cmd.SetOut(b)
    cmd.SetArgs([]string{"--help"})
    
    if err := cmd.Execute(); err != nil {
        t.Fatalf("wykonanie polecenia nie powiodło się: %v", err)
    }
}

Testowanie z różnymi konfiguracjami

func TestWithConfig(t *testing.T) {
    viper.Set("database", "/tmp/test.db")
    viper.Set("debug", true)
    
    // Uruchom swoje testy
    
    viper.Reset() // Czyszczenie
}

Generowanie skryptów kompletacji powłoki

Cobra może generować skrypty kompletacji dla różnych powłok:

// cmd/completion.go
package cmd

import (
    "os"
    "github.com/spf13/cobra"
)

var completionCmd = &cobra.Command{
    Use:   "completion [bash|zsh|fish|powershell]",
    Short: "Generuj skrypt kompletacji",
    Long: `Aby załadować kompletacje:

Bash:
  $ source <(mytasks completion bash)

Zsh:
  $ source <(mytasks completion zsh)

Fish:
  $ mytasks completion fish | source

PowerShell:
  PS> mytasks completion powershell | Out-String | Invoke-Expression
`,
    DisableFlagsInUseLine: true,
    ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
    Args: cobra.ExactValidArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        switch args[0] {
        case "bash":
            cmd.Root().GenBashCompletion(os.Stdout)
        case "zsh":
            cmd.Root().GenZshCompletion(os.Stdout)
        case "fish":
            cmd.Root().GenFishCompletion(os.Stdout, true)
        case "powershell":
            cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
        }
    },
}

func init() {
    rootCmd.AddCommand(completionCmd)
}

Budowanie i dystrybucja

Budowanie dla wielu platform

# Linux
GOOS=linux GOARCH=amd64 go build -o mytasks-linux-amd64

# macOS
GOOS=darwin GOARCH=amd64 go build -o mytasks-darwin-amd64
GOOS=darwin GOARCH=arm64 go build -o mytasks-darwin-arm64

# Windows
GOOS=windows GOARCH=amd64 go build -o mytasks-windows-amd64.exe

Zmniejszenie rozmiaru binarki

go build -ldflags="-s -w" -o mytasks

Dodanie informacji o wersji

// cmd/version.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
)

var (
    Version   = "dev"
    Commit    = "none"
    BuildTime = "unknown"
)

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "Wydrukuj informacje o wersji",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Wersja: %s\n", Version)
        fmt.Printf("Commit: %s\n", Commit)
        fmt.Printf("Zbudowane: %s\n", BuildTime)
    },
}

func init() {
    rootCmd.AddCommand(versionCmd)
}

Budowanie z informacjami o wersji:

go build -ldflags="-X 'mytasks/cmd.Version=1.0.0' \
    -X 'mytasks/cmd.Commit=$(git rev-parse HEAD)' \
    -X 'mytasks/cmd.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)'" \
    -o mytasks

Przykłady z życia

Popularne narzędzia open source zbudowane z użyciem Cobra i Viper:

  • kubectl: Narzędzie CLI dla Kubernetes
  • Hugo: Generator statycznych stron
  • GitHub CLI (gh): Oficjalne narzędzie CLI GitHub
  • Docker CLI: Zarządzanie kontenerami
  • Helm: Menedżer pakietów Kubernetes
  • Skaffold: Narzędzie do pracy z Kubernetes
  • Cobra CLI: Samoobsługowy - Cobra używa samego siebie!

Ponad tradycyjne narzędzia DevOps, możliwości CLI w Go rozszerzają się również do aplikacji AI i uczenia maszynowego. Jeśli jesteś zainteresowany tworzeniem narzędzi CLI, które interagują z dużymi modelami językowymi, sprawdź nasz przewodnik po Ograniczaniu LLM za pomocą strukturalnego wyjścia z użyciem Ollama i Go.

Przydatne zasoby

Podsumowanie

Cobra i Viper razem tworzą mocną podstawę do budowania profesjonalnych aplikacji CLI w Go. Cobra obsługuje strukturę poleceń, analizę flag i generację pomocy, podczas gdy Viper zarządza konfiguracją z wielu źródeł z inteligentnym priorytetem.

To połączenie umożliwia tworzenie narzędzi CLI, które są:

  • Łatwe w użyciu z intuicyjnymi poleceniami i automatyczną pomocą
  • Elastyczne z wieloma źródłami konfiguracji
  • Profesjonalne z kompletacją powłoki i odpowiednią obsługą błędów
  • Utrzywalne z czystą organizacją kodu
  • Przenośne na różne platformy

Nie ważne, czy tworzysz narzędzia dla programistów, narzędzia systemowe czy automatyzację DevOps, Cobra i Viper dostarczają solidnej podstawy, którą potrzebujesz do tworzenia aplikacji CLI, które użytkownicy będą uwielbiać.