1. Introdução ao 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.