1. Introduzione a Viper

golang viper

Comprendere la necessità di una soluzione di configurazione nelle applicazioni Go

Per costruire software affidabili e manutenibili, gli sviluppatori devono separare la configurazione dalla logica dell'applicazione. Questo consente di regolare il comportamento dell'applicazione senza modificare la base di codice. Una soluzione di configurazione facilita questa separazione consentendo l'esternalizzazione dei dati di configurazione.

Le applicazioni Go possono beneficiare notevolmente di un sistema del genere, specialmente quando crescono in complessità e si trovano di fronte a vari ambienti di distribuzione, come sviluppo, staging e produzione. Ogni di questi ambienti potrebbero richiedere impostazioni diverse per connessioni al database, chiavi API, numeri di porta e altro ancora. Inserire valori direttamente nel codice può essere problematico e suscettibile ad errori, poiché porta a molteplici percorsi di codice per mantenere diverse configurazioni e aumenta il rischio di esposizione di dati sensibili.

Una soluzione di configurazione come Viper affronta queste preoccupazioni fornendo un approccio unificato che supporta diverse esigenze e formati di configurazione.

Panoramica di Viper e del suo ruolo nella gestione delle configurazioni

Viper è una libreria di configurazione completa per le applicazioni Go, mirata a essere la soluzione predefinita per tutte le esigenze di configurazione. Si allinea alle pratiche stabilite nella metodologia Twelve-Factor App, che incoraggia a memorizzare la configurazione nell'ambiente per ottenere portabilità tra gli ambienti di esecuzione.

Viper svolge un ruolo fondamentale nella gestione delle configurazioni attraverso:

  • Lettura e disaccoppiamento di file di configurazione in vari formati come JSON, TOML, YAML, HCL e altri.
  • Sovrascrittura dei valori di configurazione con variabili di ambiente, aderendo così al principio della configurazione esterna.
  • Associazione e lettura dai flag della riga di comando per consentire l'impostazione dinamica delle opzioni di configurazione in fase di esecuzione.
  • Consentire di impostare valori predefiniti all'interno dell'applicazione per le opzioni di configurazione non fornite esternamente.
  • Monitoraggio delle modifiche ai file di configurazione e ricarica live, offrendo flessibilità e riducendo i tempi di inattività per le modifiche di configurazione.

2. Installazione e configurazione

Installazione di Viper utilizzando i moduli Go

Per aggiungere Viper al tuo progetto Go, assicurati che il tuo progetto utilizzi già i moduli Go per la gestione delle dipendenze. Se hai già un progetto Go, è probabile che tu abbia un file go.mod nella radice del tuo progetto. In caso contrario, puoi inizializzare i moduli Go eseguendo il comando seguente:

go mod init <nome-modulo>

Sostituisci <nome-modulo> con il nome o il percorso del tuo progetto. Una volta inizializzati i moduli Go nel tuo progetto, puoi aggiungere Viper come dipendenza:

go get github.com/spf13/viper

Questo comando scaricherà il pacchetto Viper e registrerà la sua versione nel tuo file go.mod.

Inizializzazione di Viper in un progetto Go

Per iniziare a utilizzare Viper all'interno del tuo progetto Go, devi prima importare il pacchetto e quindi creare una nuova istanza di Viper o utilizzare il singleton predefinito. Di seguito è riportato un esempio di entrambi i casi:

package main

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

func main() {
	// Utilizzo del Viper singleton, pre-configurato e pronto all'uso
	viper.SetDefault("nomeServizio", "Il mio servizio fantastico")

	// In alternativa, creazione di una nuova istanza di Viper
	mioViper := viper.New()
	mioViper.SetDefault("nomeServizio", "Il mio nuovo servizio")

	// Accesso a un valore di configurazione utilizzando il singleton
	nomeServizio := viper.GetString("nomeServizio")
	fmt.Println("Il nome del servizio è:", nomeServizio)

	// Accesso a un valore di configurazione utilizzando la nuova istanza
	nuovoNomeServizio := mioViper.GetString("nomeServizio")
	fmt.Println("Il nuovo nome del servizio è:", nuovoNomeServizio)
}

Nel codice sopra, SetDefault viene utilizzato per definire un valore predefinito per una chiave di configurazione. Il metodo GetString recupera un valore. Quando esegui questo codice, verranno stampati entrambi i nomi dei servizi che abbiamo configurato sia utilizzando l'istanza singleton che l'istanza nuova.

3. Lettura e Scrittura dei File di Configurazione

Lavorare con i file di configurazione è una caratteristica fondamentale di Viper. Consente all'applicazione di esternalizzare la propria configurazione in modo che possa essere aggiornata senza la necessità di ricompilare il codice. Di seguito, esploreremo la configurazione di vari formati e mostreremo come leggere e scrivere su questi file.

Configurazione dei formati di configurazione (JSON, TOML, YAML, HCL, ecc.)

Viper supporta diversi formati di configurazione come JSON, TOML, YAML, HCL, ecc. Per iniziare, è necessario impostare il nome e il tipo del file di configurazione che Viper deve cercare:

v := viper.New()

v.SetConfigName("app")  // Nome del file di configurazione senza estensione
v.SetConfigType("yaml") // o "json", "toml", "yml", "hcl", ecc.

// Percorsi di ricerca del file di configurazione. Aggiungi percorsi multipli se la posizione del file di configurazione varia.
v.AddConfigPath("$HOME/.appconfig") // Posizione tipica della configurazione dell'utente UNIX
v.AddConfigPath("/etc/appconfig/")  // Percorso della configurazione a livello di sistema UNIX
v.AddConfigPath(".")                // La directory di lavoro

Lettura e scrittura dei file di configurazione

Una volta che l'istanza di Viper sa dove cercare i file di configurazione e cosa cercare, è possibile chiederle di leggere la configurazione:

if err := v.ReadInConfig(); err != nil {
    if _, ok := err.(viper.ConfigFileNotFoundError); ok {
        // File di configurazione non trovato; ignorare se desiderato o gestire diversamente
        log.Printf("Nessun file di configurazione trovato. Utilizzo dei valori predefiniti e/o delle variabili di ambiente.")
    } else {
        // Il file di configurazione è stato trovato ma si è verificato un altro errore
        log.Fatalf("Errore durante la lettura del file di configurazione, %s", err)
    }
}

Per scrivere le modifiche nel file di configurazione o crearne uno nuovo, Viper offre diversi metodi. Ecco come scrivere la configurazione corrente su un file:

err := v.WriteConfig() // Scrive la configurazione corrente nel percorso predefinito impostato da `v.SetConfigName` e `v.AddConfigPath`
if err != nil {
    log.Fatalf("Errore durante la scrittura del file di configurazione, %s", err)
}

Impostazione dei valori di configurazione predefiniti

I valori predefiniti fungono da fallback nel caso in cui una chiave non sia stata impostata nel file di configurazione o dalle variabili di ambiente:

v.SetDefault("ContentDir", "contenuto")
v.SetDefault("LogLevel", "debug")
v.SetDefault("Database.Port", 5432)

// Una struttura dati più complessa per il valore predefinito
viper.SetDefault("Tassonomie", map[string]string{
    "tag":       "tag",
    "categoria":  "categorie",
})

4. Gestione delle variabili d'ambiente e dei flag

Viper non è limitato solo ai file di configurazione: può gestire anche le variabili d'ambiente e i flag della riga di comando, il che è particolarmente utile quando si trattano impostazioni specifiche dell'ambiente.

Associazione delle variabili d'ambiente e dei flag a Viper

Associazione delle variabili d'ambiente:

v.AutomaticEnv() // Cerca automaticamente le chiavi delle variabili d'ambiente che corrispondono alle chiavi di Viper

v.SetEnvPrefix("APP") // Prefisso per le variabili d'ambiente per distinguerle dalle altre
v.BindEnv("port")     // Associa la variabile d'ambiente PORT (es. APP_PORT)

// È anche possibile associare variabili d'ambiente con nomi diversi alle chiavi dell'applicazione
v.BindEnv("database_url", "DB_URL") // Ciò dice a Viper di utilizzare il valore della variabile d'ambiente DB_URL per la chiave di configurazione "database_url"

Associazione dei flag utilizzando pflag, un pacchetto Go per l'analisi dei flag:

var porta int

// Definire un flag utilizzando pflag
pflag.IntVarP(&porta, "porta", "p", 808, "Porta per l'applicazione")

// Associa il flag a una chiave di Viper
pflag.Parse()
if err := v.BindPFlag("porta", pflag.Lookup("porta")); err != nil {
    log.Fatalf("Errore durante l'associazione del flag alla chiave, %s", err)
}

Gestione delle configurazioni specifiche dell'ambiente

Un'applicazione deve spesso funzionare in modo diverso in vari ambienti (sviluppo, staging, produzione, ecc.). Viper può consumare la configurazione dalle variabili d'ambiente che possono sovrascrivere le impostazioni nel file di configurazione, consentendo configurazioni specifiche dell'ambiente:

v.SetConfigName("config") // Il nome predefinito del file di configurazione

// La configurazione può essere sovrascritta dalle variabili d'ambiente
// con il prefisso APP e il resto della chiave in maiuscolo
v.SetEnvPrefix("APP")
v.AutomaticEnv()

// In un ambiente di produzione, è possibile utilizzare la variabile d'ambiente APP_PORT per sovrascrivere la porta predefinita
fmt.Println(v.GetString("porta")) // L'output sarà il valore di APP_PORT se impostato, altrimenti il valore dal file di configurazione o il valore predefinito

Ricorda di gestire, se necessario, le differenze tra gli ambienti all'interno del codice dell'applicazione in base alle configurazioni caricate da Viper.

5. Supporto per archiviazione remota chiave/valore

Viper fornisce un robusto supporto per la gestione della configurazione dell'applicazione utilizzando archiviazioni remote di tipo chiave/valore come etcd, Consul o Firestore. Ciò consente di centralizzare le configurazioni e di aggiornarle dinamicamente tra sistemi distribuiti. Inoltre, Viper permette la gestione sicura delle configurazioni sensibili attraverso la crittografia.

Integrazione di Viper con archiviazioni remota chiave/valore (etcd, Consul, Firestore, ecc.)

Per iniziare ad utilizzare Viper con le archiviazioni remote chiave/valore, è necessario eseguire un'importazione vuota del package viper/remote nella tua applicazione Go:

import _ "github.com/spf13/viper/remote"

Ecco un esempio di integrazione con etcd:

import (
    "log"

    "github.com/spf13/viper"
    _ "github.com/spf13/viper/remote"
)

func initRemoteConfig() {
    viper.SetConfigType("json") // Imposta il tipo di file di configurazione remota
    viper.AddRemoteProvider("etcd", "http://127...1:4001", "/config/myapp.json")
  
    err := viper.ReadRemoteConfig() // Tentativo di leggere la configurazione remota
    if err != nil {
        log.Fatalf("Impossibile leggere la configurazione remota: %v", err)
    }
  
    log.Println("Lettura della configurazione remota avvenuta con successo")
}

func main() {
    initRemoteConfig()
    // La logica dell'applicazione va qui
}

In questo esempio, Viper si connette ad un server etcd in esecuzione su http://127...1:4001 e legge la configurazione situata in /config/myapp.json. Quando si lavora con altri archivi come Consul, sostituire "etcd" con "consul" e adattare i parametri specifici del provider di conseguenza.

Gestione delle configurazioni crittografate

Le configurazioni sensibili, come le chiavi API o le credenziali del database, non devono essere memorizzate in testo normale. Viper consente di memorizzare configurazioni crittografate in un archivio chiave/valore e di decifrarle nella tua applicazione.

Per utilizzare questa funzionalità, assicurarsi che le impostazioni crittografate siano memorizzate nel proprio archivio chiave/valore. Quindi sfruttare il metodo AddSecureRemoteProvider di Viper. Ecco un esempio di utilizzo con etcd:

import (
    "log"

    "github.com/spf13/viper"
    _ "github.com/spf13/viper/remote"
)

func initSecureRemoteConfig() {
    const secretKeyring = "/path/to/secret/keyring.gpg" // Percorso del file di chiavi segrete
  
    viper.SetConfigType("json")
    viper.AddSecureRemoteProvider("etcd", "http://127...1:4001", "/config/myapp.json", secretKeyring)
  
    err := viper.ReadRemoteConfig()
    if err != nil {
        log.Fatalf("Impossibile leggere la configurazione remota: %v", err)
    }

    log.Println("Lettura e decodifica della configurazione remota avvenute con successo")
}

func main() {
    initSecureRemoteConfig()
    // La logica dell'applicazione va qui
}

Nell'esempio sopra, viene utilizzato AddSecureRemoteProvider, specificando il percorso di un keyring GPG che contiene le chiavi necessarie per la decrittografia.

6. Monitoraggio e gestione delle modifiche della configurazione

Una delle potenti funzionalità di Viper è la sua capacità di monitorare e rispondere alle modifiche della configurazione in tempo reale, senza riavviare l'applicazione.

Monitorare le modifiche della configurazione e rileggere le configurazioni

Viper utilizza il pacchetto fsnotify per monitorare le modifiche al file di configurazione. È possibile configurare un watcher per attivare eventi ogni volta che il file di configurazione cambia:

import (
    "log"

    "github.com/fsnotify/fsnotify"
    "github.com/spf13/viper"
)

func watchConfig() {
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        log.Printf("File di configurazione modificato: %s", e.Name)
        // Qui è possibile leggere la configurazione aggiornata se necessario
        // Eseguire qualsiasi azione come reinizializzare servizi o aggiornare variabili
    })
}

func main() {
    viper.SetConfigName("myapp")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        log.Fatalf("Errore nella lettura del file di configurazione, %s", err)
    }

    watchConfig()
    // La logica dell'applicazione va qui
}

Trigger per l'Aggiornamento delle Configurazioni in un'applicazione in esecuzione

In un'applicazione in esecuzione, potresti voler aggiornare le configurazioni in risposta a vari trigger, come un segnale, un lavoro basato sul tempo o una richiesta API. È possibile strutturare l'applicazione in modo che aggiorni il suo stato interno in base alla rilettura delle configurazioni di Viper:

import (
    "os"
    "os/signal"
    "syscall"
    "time"
    "log"

    "github.com/spf13/viper"
)

func setupSignalHandler() {
    signalChannel := make(chan os.Signal, 1)
    signal.Notify(signalChannel, syscall.SIGHUP) // Ascolto del segnale SIGHUP

    go func() {
        for {
            sig := <-signalChannel
            if sig == syscall.SIGHUP {
                log.Println("Ricevuto segnale SIGHUP. Ricaricamento della configurazione...")
                err := viper.ReadInConfig() // Rilettura della configurazione
                if err != nil {
                    log.Printf("Errore nella rilettura della configurazione: %s", err)
                } else {
                    log.Println("Configurazione ricaricata con successo.")
                    // Riconfigura qui l'applicazione sulla base della nuova configurazione
                }
            }
        }
    }()
}

func main() {
    viper.SetConfigName("myapp")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        log.Fatalf("Errore nella lettura del file di configurazione, %s", err)
    }

    setupSignalHandler()
    for {
        // Logica principale dell'applicazione
        time.Sleep(10 * time.Second) // Simulazione di un certo lavoro
    }
}

In questo esempio, stiamo impostando un gestore per ascoltare il segnale SIGHUP. Quando viene ricevuto, Viper ricarica il file di configurazione e l'applicazione dovrebbe quindi aggiornare le sue configurazioni o lo stato come necessario.

Ricorda sempre di testare queste configurazioni per garantire che la tua applicazione possa gestire gli aggiornamenti dinamici in modo efficiente.