CLI-apps bouwen in Go met Cobra & Viper

CLI-ontwikkeling in Go met Cobra- en Viper-frameworks

Inhoud

Command-line interface (CLI) toepassingen zijn essentiële tools voor ontwikkelaars, systeembeheerders en DevOps-professionals. Twee Go-bibliotheken zijn geworden de standaard voor CLI-ontwikkeling in Go: Cobra voor commandostructuur en Viper voor configuratiebeheer.

Go is ontstaan als een uitstekende taal voor het bouwen van CLI-tools vanwege de prestaties, eenvoudige implementatie en cross-platform ondersteuning.

Tetris

Waarom Go kiezen voor CLI-toepassingen

Go biedt overtuigende voordelen voor CLI-ontwikkeling:

  • Eén-binair distributie: Geen runtime-afhankelijkheden of pakketbeheerders vereist
  • Snelle uitvoering: Native compilatie biedt uitstekende prestaties
  • Cross-platform ondersteuning: Eenvoudige compilatie voor Linux, macOS, Windows en meer
  • Sterke standaardbibliotheek: Rijke tooling voor bestands-I/O, netwerken en tekstverwerking
  • Concurrentie: Ingebouwde goroutines voor parallele bewerkingen
  • Statische typen: Fouten opvangen tijdens de compilatie

Populaire CLI-tools gebouwd met Go omvatten Docker, Kubernetes (kubectl), Hugo, Terraform en GitHub CLI. Als je nieuw bent in Go of een snelle naslag nodig hebt, bekijk dan onze Go Cheat Sheet voor essentiële Go-syntaxis en patronen.

Inleiding tot Cobra

Cobra is een bibliotheek die een eenvoudige interface biedt voor het maken van krachtige moderne CLI-toepassingen. Gemaakt door Steve Francia (spf13), de zelfde auteur achter Hugo en Viper, wordt Cobra gebruikt in veel van de populairste Go-projecten.

Belangrijke Cobra-functies

Commandostructuur: Cobra implementeert het commandompatroon, waardoor je toepassingen kunt maken met commando’s en subcommando’s (zoals git commit of docker run).

Flagbeheer: Zowel lokale als blijvende flags met automatische parsing en typeconversie.

Automatische hulp: Genereert hulp tekst en gebruiksinformatie automatisch.

Slimme suggesties: Geeft suggesties wanneer gebruikers typos maken (“Meende je ‘status’?”).

Shell-completies: Genereert compleet scripts voor bash, zsh, fish en PowerShell.

Flexibele uitvoer: Werkt naadloos met aangepaste formatters en uitvoerstijlen.

Inleiding tot Viper

Viper is een volledige configuratieoplossing voor Go-toepassingen, ontworpen om naadloos te werken met Cobra. Het beheert configuratie van meerdere bronnen met een duidelijke voorkeursvolgorde.

Belangrijke Viper-functies

Meerdere configuratiebronnen:

  • Configuratiebestanden (JSON, YAML, TOML, HCL, INI, envfile, Java properties)
  • Omgevingsvariabelen
  • Commando-flags
  • Verwijderde configuratiesystemen (etcd, Consul)
  • Standaardwaarden

Configuratievoorkeursvolgorde:

  1. Explicit calls to Set
  2. Commando-flags
  3. Omgevingsvariabelen
  4. Configuratiebestand
  5. Sleutel/waardeopslag
  6. Standaardwaarden

Live watching: Bewaak configuratiebestanden en herlaad automatisch wanneer wijzigingen optreden.

Typeconversie: Automatische conversie naar verschillende Go-typen (string, int, bool, duur, enz.).

Aan de slag: Installatie

Eerst, initialiseer een nieuw Go-modul en installeer beide bibliotheken:

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

Optioneel, installeer de Cobra CLI-generator voor het opzetten:

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

Bouwen van je eerste CLI-toepassing

Laten we een praktijkvoorbeeld bouwen: een taakbeheer CLI-tool met configuratieondersteuning.

Projectstructuur

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

Hoofdinstelling

// main.go
package main

import "mytasks/cmd"

func main() {
    cmd.Execute()
}

Hoofdcommando met Viper-integratie

// 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: "Een eenvoudig taakbeheer CLI",
    Long: `MyTasks is een CLI-taakbeheerder die je helpt om 
je dagelijkse taken gemakkelijk te organiseren. Gemaakt met Cobra en 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", "", 
        "configbestand (standaard is $HOME/.mytasks.yaml)")
    rootCmd.PersistentFlags().String("db", "", 
        "locatie van het databestand")
    
    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("Gebruikte configuratiebestand:", viper.ConfigFileUsed())
    }
}

Subcommando’s toevoegen

// cmd/add.go
package cmd

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

var priority string

var addCmd = &cobra.Command{
    Use:   "add [taakbeschrijving]",
    Short: "Voeg een nieuwe taak toe",
    Args:  cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        taak := args[0]
        db := viper.GetString("database")
        
        fmt.Printf("Toevoegen van taak: %s\n", taak)
        fmt.Printf("Prioriteit: %s\n", priority)
        fmt.Printf("Database: %s\n", db)
        
        // Hier zou je de werkelijke taakopslag implementeren
    },
}

func init() {
    rootCmd.AddCommand(addCmd)
    
    addCmd.Flags().StringVarP(&priority, "priority", "p", "medium", 
        "Taakprioriteit (laag, gemiddeld, hoog)")
}

Lijstcommando

// 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: "Toon alle taken",
    Run: func(cmd *cobra.Command, args []string) {
        db := viper.GetString("database")
        
        fmt.Printf("Lijst met taken van: %s\n", db)
        fmt.Printf("Toon voltooide: %v\n", showCompleted)
        
        // Hier zou je de werkelijke taaklijst implementeren
    },
}

func init() {
    rootCmd.AddCommand(listCmd)
    
    listCmd.Flags().BoolVarP(&showCompleted, "completed", "c", false, 
        "Toon voltooide taken")
}

Implementatie van gegevensopslag

Voor een productie taakbeheer CLI, zul je werkelijke gegevensopslag moeten implementeren. Hoewel we hier een eenvoudige databestandslocatie gebruiken, heb je verschillende opties voor het opslaan van gegevens:

  • SQLite: Lichte, serverloze database ideaal voor CLI-tools
  • PostgreSQL/MySQL: Volledig uitgeruste databases voor complexere toepassingen
  • JSON/YAML-bestanden: Eenvoudige bestandsgebaseerde opslag voor lichte behoeften
  • Ingewikkelde databases: BoltDB, BadgerDB voor sleutel-waardeopslag

Als je werkt met relationele databases zoals PostgreSQL, wil je een ORM of querybuilder gebruiken. Voor een uitgebreid overzicht van Go-databasebibliotheken, zie onze gids over Vergelijken van Go-ORMs voor PostgreSQL: GORM vs Ent vs Bun vs sqlc.

Geavanceerde configuratie met Viper

Voorbeeld van configuratiebestand

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

notifications:
  enabled: true
  sound: true

priorities:
  high: red
  medium: yellow
  low: green

Lezen van geneste configuratie

func getNotificationSettings() {
    enabled := viper.GetBool("notifications.enabled")
    sound := viper.GetBool("notifications.sound")
    
    fmt.Printf("Notificaties ingeschakeld: %v\n", enabled)
    fmt.Printf("Geluid ingeschakeld: %v\n", sound)
}

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

Omgevingsvariabelen

Viper leest automatisch omgevingsvariabelen met de geconfigureerde voorvoegsel:

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

Live configuratieherlaad

func watchConfig() {
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        fmt.Println("Configbestand gewijzigd:", e.Name)
        // Herlaad configuratie-afhankelijke componenten
    })
}

Beste praktijken

1. Gebruik de Cobra-generator voor consistentie

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

2. Organiseer commando’s in aparte bestanden

Houd elk commando in zijn eigen bestand onder de cmd/ map voor onderhoudbaarheid.

3. Gebruik blijvende flags

Gebruik blijvende flags voor opties die van toepassing zijn op alle subcommando’s:

rootCmd.PersistentFlags().StringP("output", "o", "text", 
    "Uitvoerformaat (tekst, json, yaml)")

4. Implementeer juiste foutafhandeling

var rootCmd = &cobra.Command{
    Use:   "myapp",
    Short: "Mijn toepassing",
    RunE: func(cmd *cobra.Command, args []string) error {
        if err := doSomething(); err != nil {
            return fmt.Errorf("mislukt om iets te doen: %w", err)
        }
        return nil
    },
}

5. Geef betekenisvolle hulp tekst

var cmd = &cobra.Command{
    Use:   "deploy [omgeving]",
    Short: "Implementeer toepassing in de opgegeven omgeving",
    Long: `Implementeer bouwt en implementeert je toepassing in de 
opgegeven omgeving. Ondersteunde omgevingen zijn:
  - ontwikkeling (dev)
  - testomgeving
  - productie (prod)`,
    Example: `  myapp deploy testomgeving
  myapp deploy productie --versie=1.2.3`,
}

6. Stel logische standaardwaarden in

func init() {
    viper.SetDefault("poort", 8080)
    viper.SetDefault("time-out", "30s")
    viper.SetDefault("herproberen", 3)
}

7. Valideer configuratie

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

Testen van CLI-toepassingen

Testen van commando’s

// 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("commando uitvoering mislukt: %v", err)
    }
}

Testen met verschillende configuraties

func TestWithConfig(t *testing.T) {
    viper.Set("database", "/tmp/test.db")
    viper.Set("debug", true)
    
    // Voer je tests uit
    
    viper.Reset() // Opschonen
}

Genereren van shell-completies

Cobra kan compleet scripts genereren voor verschillende shells:

// cmd/completion.go
package cmd

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

var completionCmd = &cobra.Command{
    Use:   "completion [bash|zsh|fish|powershell]",
    Short: "Genereer compleet script",
    Long: `Om compleet scripts te laden:

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

Bouwen en distributie

Bouwen voor meerdere platforms

# 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

Verminder de bestandsgrootte

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

Voeg versieinformatie toe

// cmd/version.go
package cmd

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

var (
    Versie   = "dev"
    Commit    = "none"
    BuildTijd = "onbekend"
)

var versionCmd = &cobra.Command{
    Use:   "versie",
    Short: "Toon versieinformatie",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Versie: %s\n", Versie)
        fmt.Printf("Commit: %s\n", Commit)
        fmt.Printf("Gebouwd: %s\n", BuildTijd)
    },
}

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

Bouw met versieinformatie:

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

Reële voorbeelden

Populaire open-source tools gebouwd met Cobra en Viper:

  • kubectl: Kubernetes commando-line tool
  • Hugo: Statische site generator
  • GitHub CLI (gh): Officiële GitHub CLI
  • Docker CLI: Containerbeheer
  • Helm: Kubernetes pakketbeheerder
  • Skaffold: Kubernetes ontwikkelingswerkstroomtool
  • Cobra CLI: Self-hosting - Cobra gebruikt zichzelf!

Buiten traditionele DevOps-tools, breiden Go’s CLI-mogelijkheden zich uit naar AI en machine learning toepassingen. Als je geïnteresseerd bent in het bouwen van CLI-tools die werken met grote taalmodellen, bekijk dan onze gids over Beperken van LLMs met Structured Output met Ollama en Go, die laat zien hoe je Go-toepassingen kunt bouwen die werken met AI-modellen.

Nuttige bronnen

Conclusie

Cobra en Viper samen vormen een krachtige basis voor het bouwen van professionele CLI-toepassingen in Go. Cobra beheert de commandostructuur, flag parsing en hulpgeneratie, terwijl Viper configuratie beheert van meerdere bronnen met slimme voorkeursvolgorde.

Deze combinatie stelt je in staat om CLI-tools te maken die zijn:

  • Eenvoudig te gebruiken met intuïtieve commando’s en automatische hulp
  • Flexibel met meerdere configuratiebronnen
  • Professioneel met shell-completies en juiste foutafhandeling
  • Onderhoudbaar met een nette codeorganisatie
  • Overdraagbaar over verschillende platforms

Of je nu ontwikkelaartools, systeemhulpmiddelen of DevOps-automatisering bouwt, bieden Cobra en Viper de solide basis die je nodig hebt om CLI-toepassingen te maken die gebruikers zullen waarderen.