1. Introdução ao Viper

golang viper

Compreensão da necessidade de uma solução de configuração em aplicações Go

Para construir software confiável e de fácil manutenção, os desenvolvedores precisam separar a configuração da lógica da aplicação. Isso permite ajustar o comportamento da aplicação sem alterar o código-fonte. Uma solução de configuração possibilita essa separação, facilitando a externalização dos dados de configuração.

Aplicações Go podem se beneficiar significativamente desse sistema, especialmente à medida que crescem em complexidade e enfrentam diversos ambientes de implantação, como desenvolvimento, staging e produção. Cada um desses ambientes pode exigir configurações diferentes para conexões de banco de dados, chaves de API, números de porta e outros. Codificar esses valores pode ser problemático e propenso a erros, pois leva a múltiplos caminhos de código para manter diferentes configurações e aumenta o risco de exposição de dados sensíveis.

Uma solução de configuração como o Viper aborda essas preocupações ao oferecer uma abordagem unificada que suporta diversas necessidades e formatos de configuração.

Visão geral do Viper e seu papel na gestão de configurações

O Viper é uma biblioteca abrangente de configuração para aplicações Go, visando ser a solução padrão para todas as necessidades de configuração. Ele está alinhado com as práticas estipuladas na metodologia Twelve-Factor App, que incentiva o armazenamento de configurações no ambiente para alcançar portabilidade entre os ambientes de execução.

O Viper desempenha um papel fundamental na gestão de configurações, realizando as seguintes atividades:

  • Leitura e deserialização de arquivos de configuração em vários formatos, como JSON, TOML, YAML, HCL e mais.
  • Sobrescrita de valores de configuração com variáveis de ambiente, aderindo ao princípio de configuração externa.
  • Vinculação e leitura de flags de linha de comando para permitir a configuração dinâmica de opções de configuração em tempo de execução.
  • Permitindo a definição de valores padrão dentro da aplicação para opções de configuração não fornecidas externamente.
  • Observação de mudanças em arquivos de configuração e recarga em tempo real, oferecendo flexibilidade e reduzindo o tempo de inatividade para alterações de configuração.

2. Instalação e Configuração

Instalando o Viper usando Go modules

Para adicionar o Viper ao seu projeto Go, certifique-se de que seu projeto esteja usando Go modules para gerenciamento de dependências. Se já tiver um projeto Go, é provável que tenha um arquivo go.mod na raiz do seu projeto. Caso contrário, você pode inicializar os Go modules executando o seguinte comando:

go mod init <nome-do-módulo>

Substitua <nome-do-módulo> pelo nome ou caminho do seu projeto. Uma vez que os Go Modules estejam inicializados em seu projeto, você pode adicionar o Viper como uma dependência:

go get github.com/spf13/viper

Este comando irá buscar o pacote do Viper e registrar sua versão no seu arquivo go.mod.

Inicializando o Viper em um projeto Go

Para começar a usar o Viper em seu projeto Go, você precisa primeiro importar o pacote e então criar uma nova instância do Viper ou usar o singleton predefinido. Abaixo está um exemplo de como fazer ambos:

package main

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

func main() {
	// Usando o singleton do Viper, que está pré-configurado e pronto para uso
	viper.SetDefault("nomeDoServico", "Meu Serviço Incrível")

	// Alternativamente, criando uma nova instância do Viper
	meuViper := viper.New()
	meuViper.SetDefault("nomeDoServico", "Meu Novo Serviço")

	// Acessando um valor de configuração usando o singleton
	nomeDoServico := viper.GetString("nomeDoServico")
	fmt.Println("O nome do serviço é:", nomeDoServico)

	// Acessando um valor de configuração usando a nova instância
	novoNomeDoServico := meuViper.GetString("nomeDoServico")
	fmt.Println("O novo nome do serviço é:", novoNomeDoServico)
}

No código acima, SetDefault é usado para definir um valor padrão para uma chave de configuração. O método GetString recupera um valor. Quando você executa este código, ele imprime tanto os nomes de serviço que configuramos usando tanto a instância singleton quanto a nova instância.

3. Leitura e Escrita de Arquivos de Configuração

Trabalhar com arquivos de configuração é uma funcionalidade central do Viper. Isso permite que sua aplicação externalize sua configuração, para que possa ser atualizada sem a necessidade de recompilar o código. Abaixo, vamos explorar a configuração de vários formatos de configuração e mostrar como ler e escrever nesses arquivos.

Configuração de formatos (JSON, TOML, YAML, HCL, etc.)

O Viper suporta vários formatos de configuração, como JSON, TOML, YAML, HCL, etc. Para começar, você deve definir o nome e o tipo do arquivo de configuração que o Viper deve procurar:

v := viper.New()

v.SetConfigName("app")  // Nome do arquivo de configuração sem a extensão
v.SetConfigType("yaml") // ou "json", "toml", "yml", "hcl", etc.

// Caminhos de busca do arquivo de configuração. Adicione vários caminhos se a localização do arquivo de configuração variar.
v.AddConfigPath("$HOME/.appconfig") // Local típico de configuração do usuário UNIX
v.AddConfigPath("/etc/appconfig/")  // Caminho de configuração do sistema UNIX
v.AddConfigPath(".")                // O diretório de trabalho

Leitura e escrita de arquivos de configuração

Uma vez que a instância do Viper saiba onde procurar pelos arquivos de configuração e o que procurar, você pode pedir a ele para ler a configuração:

if err := v.ReadInConfig(); err != nil {
    if _, ok := err.(viper.ConfigFileNotFoundError); ok {
        // Arquivo de configuração não encontrado; ignore se desejado ou manipule de outra forma
        log.Printf("Nenhum arquivo de configuração encontrado. Usando valores padrão e/ou variáveis de ambiente.")
    } else {
        // Arquivo de configuração encontrado, mas outro erro foi encontrado
        log.Fatalf("Erro ao ler o arquivo de configuração, %s", err)
    }
}

Para escrever modificações de volta para o arquivo de configuração, ou para criar um novo, o Viper oferece vários métodos. Veja como escrever a configuração atual para um arquivo:

err := v.WriteConfig() // Escreve a configuração atual no caminho predefinido definido por `v.SetConfigName` e `v.AddConfigPath`
if err != nil {
    log.Fatalf("Erro ao escrever o arquivo de configuração, %s", err)
}

Estabelecimento de valores de configuração padrão

Os valores padrão funcionam como substituições caso uma chave não tenha sido definida no arquivo de configuração ou por variáveis de ambiente:

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

// Uma estrutura de dados mais complexa para o valor padrão
viper.SetDefault("Taxonomies", map[string]string{
    "tag":       "tags",
    "category":  "categories",
})

4. Gerenciando Variáveis de Ambiente e Flags

O Viper não se limita apenas a arquivos de configuração—ele também pode gerenciar variáveis de ambiente e flags de linha de comando, o que é particularmente útil ao lidar com configurações específicas do ambiente.

Vinculação de variáveis de ambiente e flags ao Viper

Vinculando variáveis de ambiente:

v.AutomaticEnv() // Procura automaticamente por chaves de variáveis de ambiente que correspondem às chaves do Viper

v.SetEnvPrefix("APP") // Prefixo para variáveis de ambiente para distingui-las das outras
v.BindEnv("port")     // Vincula a variável de ambiente PORT (por exemplo, APP_PORT)

// Você também pode fazer correspondência entre variáveis de ambiente com diferentes nomes e chaves em seu aplicativo
v.BindEnv("database_url", "DB_URL") // Isso diz ao Viper para usar o valor da variável de ambiente DB_URL para a chave de configuração "database_url"

Vinculando flags usando pflag, um pacote Go para análise de flags:

var port int

// Define uma flag usando pflag
pflag.IntVarP(&port, "port", "p", 808, "Porta para a aplicação")

// Vincula a flag a uma chave do Viper
pflag.Parse()
if err := v.BindPFlag("port", pflag.Lookup("port")); err != nil {
    log.Fatalf("Erro ao vincular a flag à chave, %s", err)
}

Manipulação de configurações específicas do ambiente

Um aplicativo muitas vezes precisa funcionar de forma diferente em vários ambientes (desenvolvimento, staging, produção, etc.). O Viper pode consumir configurações de variáveis de ambiente que podem substituir as configurações no arquivo de configuração, permitindo configurações específicas do ambiente:

v.SetConfigName("config") // Nome padrão do arquivo de configuração

// A configuração pode ser substituída por variáveis de ambiente com o prefixo APP e o resto da chave em maiúsculas
v.SetEnvPrefix("APP")
v.AutomaticEnv()

// Em um ambiente de produção, você pode usar a variável de ambiente APP_PORT para substituir a porta padrão
fmt.Println(v.GetString("port")) // A saída será o valor de APP_PORT se estiver definido, caso contrário, o valor do arquivo de configuração ou o padrão

Lembre-se de lidar com as diferenças entre os ambientes dentro do código do seu aplicativo, se necessário, com base nas configurações carregadas pelo Viper.

5. Suporte para Armazenamento Remoto de Chave/Valor

O Viper oferece suporte robusto para gerenciar a configuração da aplicação usando armazenamentos remotos de chave/valor, como etcd, Consul ou Firestore. Isso permite que as configurações sejam centralizadas e atualizadas dinamicamente em sistemas distribuídos. Além disso, o Viper permite o manuseio seguro de configurações sensíveis por meio de criptografia.

Integrando o Viper com Armazenamentos Remotos de Chave/Valor (etcd, Consul, Firestore, etc.)

Para começar a usar o Viper com armazenamentos remotos de chave/valor, você precisa realizar uma importação vazia do pacote viper/remote em sua aplicação Go:

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

Vamos ver um exemplo de integração com etcd:

import (
    "log"

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

func initRemoteConfig() {
    viper.SetConfigType("json") // Define o tipo do arquivo de configuração remoto
    viper.AddRemoteProvider("etcd", "http://127...1:4001", "/config/myapp.json")
  
    err := viper.ReadRemoteConfig() // Tenta ler a configuração remota
    if err != nil {
        log.Fatalf("Falha ao ler a configuração remota: %v", err)
    }
  
    log.Println("Configuração remota lida com sucesso")
}

func main() {
    initRemoteConfig()
    // Sua lógica de aplicativo aqui
}

Neste exemplo, o Viper se conecta a um servidor etcd em execução em http://127...1:4001 e lê a configuração localizada em /config/myapp.json. Ao trabalhar com outros armazenamentos como Consul, substitua "etcd" por "consul" e ajuste os parâmetros específicos do provedor conforme necessário.

Gerenciando Configurações Criptografadas

Configurações sensíveis, como chaves de API ou credenciais de banco de dados, não devem ser armazenadas em texto simples. O Viper permite que configurações criptografadas sejam armazenadas em um armazenamento de chave/valor e descriptografadas em sua aplicação.

Para usar esse recurso, garanta que as configurações criptografadas sejam armazenadas em seu armazenamento de chave/valor. Em seguida, utilize o AddSecureRemoteProvider do Viper. Aqui está um exemplo de como utilizar isso com etcd:

import (
    "log"

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

func initSecureRemoteConfig() {
    const secretKeyring = "/caminho/para/chave/secreta.gpg" // Caminho para o arquivo de chave
  
    viper.SetConfigType("json")
    viper.AddSecureRemoteProvider("etcd", "http://127...1:4001", "/config/myapp.json", secretKeyring)
  
    err := viper.ReadRemoteConfig()
    if err != nil {
        log.Fatalf("Não foi possível ler a configuração remota: %v", err)
    }

    log.Println("Configuração remota lida e descriptografada com sucesso")
}

func main() {
    initSecureRemoteConfig()
    // Sua lógica de aplicativo aqui
}

No exemplo acima, AddSecureRemoteProvider é utilizado, especificando o caminho para um chaveiro GPG que contém as chaves necessárias para descriptografia.

6. Observando e Lidando com Alterações na Configuração

Uma das funcionalidades poderosas do Viper é sua capacidade de monitorar e responder a alterações na configuração em tempo real, sem reiniciar a aplicação.

Monitorando Alterações na Configuração e Releitura das Configurações

O Viper usa o pacote fsnotify para observar alterações no arquivo de configuração. Você pode configurar um observador para acionar eventos sempre que o arquivo de configuração mudar:

import (
    "log"

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

func watchConfig() {
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        log.Printf("Arquivo de configuração alterado: %s", e.Name)
        // Aqui você pode ler a configuração atualizada, se necessário
        // Realize qualquer ação, como reinicializar serviços ou atualizar variáveis
    })
}

func main() {
    viper.SetConfigName("myapp")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        log.Fatalf("Erro ao ler o arquivo de configuração, %s", err)
    }

    watchConfig()
    // Sua lógica de aplicativo aqui
}

Gatilhos para Atualização de Configurações em uma Aplicação em Execução

Em uma aplicação em execução, pode ser necessário atualizar as configurações em resposta a vários gatilhos, como um sinal, uma tarefa baseada em tempo ou uma solicitação de API. É possível estruturar a aplicação para atualizar seu estado interno com base na re-leitura das configurações do Viper:

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

    "github.com/spf13/viper"
)

func setupSignalHandler() {
    canalDeSinal := make(chan os.Signal, 1)
    signal.Notify(canalDeSinal, syscall.SIGHUP) // Ouvindo o sinal SIGHUP

    go func() {
        for {
            sig := <-canalDeSinal
            if sig == syscall.SIGHUP {
                log.Println("Recebido sinal SIGHUP. Recarregando configuração...")
                err := viper.ReadInConfig() // Re-leitura da configuração
                if err != nil {
                    log.Printf("Erro ao re-lêr configuração: %s", err)
                } else {
                    log.Println("Configuração recarregada com sucesso.")
                    // Reconfigure sua aplicação com base na nova configuração aqui
                }
            }
        }
    }()
}

func main() {
    viper.SetConfigName("minhaapp")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        log.Fatalf("Erro ao ler arquivo de configuração, %s", err)
    }

    setupSignalHandler()
    for {
        // Lógica principal da aplicação
        time.Sleep(10 * time.Second) // Simular algum trabalho
    }
}

Neste exemplo, estamos configurando um manipulador para ouvir o sinal SIGHUP. Quando recebido, o Viper recarrega o arquivo de configuração e a aplicação deve então atualizar suas configurações ou estado, conforme necessário.

Sempre lembre-se de testar estas configurações para garantir que sua aplicação possa lidar com atualizações dinâmicas de forma adequada.