Golang

Artigo 40 — Variáveis de ambiente e configuração com Viper Já leu

12 min de leitura

Artigo 40 — Variáveis de ambiente e configuração com Viper
Artigo 40 — Variáveis de ambiente e configuração com Viper Configuração: o elo entre código e ambiente Um sistema bem projetado separa código de configuração.

Artigo 40 — Variáveis de ambiente e configuração com Viper

Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 7 — Ferramentas, CLI e Qualidade


Configuração: o elo entre código e ambiente

Um sistema bem projetado separa código de configuração. O mesmo binário deve funcionar em desenvolvimento com um banco SQLite local, em staging com PostgreSQL compartilhado e em produção com um cluster gerenciado — sem recompilar, sem alterar código. A configuração é o que muda, não o programa.

A metodologia The Twelve-Factor App estabelece que configurações devem vir de variáveis de ambiente. Na prática, sistemas reais combinam múltiplas fontes: arquivos para configurações padrão, variáveis de ambiente para sobrescritas por ambiente, flags de linha de comando para sobrescritas pontuais. O Viper unifica todas essas fontes com precedência bem definida.


Abordagem 1: os.Getenv e os.LookupEnv

Para aplicações simples, a biblioteca padrão é suficiente:

package main

import (
    "fmt"
    "os"
    "strconv"
    "time"
)

type Config struct {
    Servidor ServidorConfig
    Banco    BancoConfig
    JWT      JWTConfig
}

type ServidorConfig struct {
    Host         string
    Porta        int
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
}

type BancoConfig struct {
    URL          string
    MaxConexoes  int
    MaxOciosas   int
}

type JWTConfig struct {
    Segredo     string
    Expiracao   time.Duration
}

func carregarConfigEnv() (*Config, error) {
    porta, err := strconv.Atoi(getEnv("SERVIDOR_PORTA", "8080"))
    if err != nil {
        return nil, fmt.Errorf("SERVIDOR_PORTA inválida: %w", err)
    }

    maxConexoes, _ := strconv.Atoi(getEnv("DB_MAX_CONEXOES", "25"))
    maxOciosas, _ := strconv.Atoi(getEnv("DB_MAX_OCIOSAS", "10"))

    segredo, existe := os.LookupEnv("JWT_SEGREDO")
    if !existe || segredo == "" {
        return nil, fmt.Errorf("JWT_SEGREDO é obrigatório")
    }

    dbURL, existe := os.LookupEnv("DATABASE_URL")
    if !existe || dbURL == "" {
        return nil, fmt.Errorf("DATABASE_URL é obrigatório")
    }

    return &Config{
        Servidor: ServidorConfig{
            Host:         getEnv("SERVIDOR_HOST", "0.0.0.0"),
            Porta:        porta,
            ReadTimeout:  getDuration("SERVIDOR_READ_TIMEOUT", 5*time.Second),
            WriteTimeout: getDuration("SERVIDOR_WRITE_TIMEOUT", 10*time.Second),
        },
        Banco: BancoConfig{
            URL:         dbURL,
            MaxConexoes: maxConexoes,
            MaxOciosas:  maxOciosas,
        },
        JWT: JWTConfig{
            Segredo:   segredo,
            Expiracao: getDuration("JWT_EXPIRACAO", 24*time.Hour),
        },
    }, nil
}

func getEnv(chave, padrao string) string {
    if v := os.Getenv(chave); v != "" {
        return v
    }
    return padrao
}

func getDuration(chave string, padrao time.Duration) time.Duration {
    v := os.Getenv(chave)
    if v == "" {
        return padrao
    }
    d, err := time.ParseDuration(v)
    if err != nil {
        return padrao
    }
    return d
}

Abordagem 2: arquivo .env com godotenv

Em desenvolvimento, variáveis de ambiente são frequentemente gerenciadas via arquivo .env:

go get github.com/joho/godotenv
# .env — nunca commitar em repositórios públicos
APP_ENV=desenvolvimento
SERVIDOR_PORTA=8080
DATABASE_URL=postgres://usuario:senha@localhost:5432/app?sslmode=disable
JWT_SEGREDO=meu-segredo-desenvolvimento-nao-usar-em-producao
LOG_LEVEL=debug
REDIS_URL=redis://localhost:6379
package main

import (
    "log"
    "os"

    "github.com/joho/godotenv"
)

func carregarDotEnv() {
    env := os.Getenv("APP_ENV")

    // Carrega arquivo específico do ambiente se existir
    if env != "" {
        godotenv.Load(".env." + env)
    }

    // Carrega .env padrão (não sobrescreve valores já definidos)
    if err := godotenv.Load(); err != nil {
        if os.IsNotExist(err) {
            log.Println("Arquivo .env não encontrado — usando variáveis do sistema")
            return
        }
        log.Printf("Aviso: erro ao carregar .env: %v", err)
    }
}

Arquivos por ambiente:

.env                    ← padrões de desenvolvimento
.env.staging           ← sobrescritas para staging
.env.test              ← sobrescritas para testes
.gitignore             ← .env listado aqui

Abordagem 3: Viper — configuração completa

O Viper é a solução mais completa para configuração em Go. Suporta arquivos YAML, JSON, TOML, INI e HCL, variáveis de ambiente, flags do Cobra, valores padrão e watching de arquivo para recarga em tempo real.

go get github.com/spf13/viper

Arquivo de configuração

# config/config.yaml
app:
  nome: "Minha API"
  versao: "1.0.0"
  ambiente: desenvolvimento

servidor:
  host: "0.0.0.0"
  porta: 8080
  read_timeout: "5s"
  write_timeout: "10s"
  idle_timeout: "60s"

banco:
  url: "postgres://usuario:senha@localhost:5432/app"
  max_conexoes: 25
  max_ociosas: 10
  max_vida: "5m"
  max_ocioso: "2m"

cache:
  url: "redis://localhost:6379"
  ttl: "1h"

jwt:
  expiracao: "24h"

log:
  nivel: "info"
  formato: "json"

Carregando com Viper

package config

import (
    "fmt"
    "strings"
    "time"

    "github.com/spf13/viper"
)

type Config struct {
    App      AppConfig
    Servidor ServidorConfig
    Banco    BancoConfig
    Cache    CacheConfig
    JWT      JWTConfig
    Log      LogConfig
}

type AppConfig struct {
    Nome      string
    Versao    string
    Ambiente  string
}

type ServidorConfig struct {
    Host         string
    Porta        int
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
    IdleTimeout  time.Duration
}

type BancoConfig struct {
    URL        string
    MaxConexoes int
    MaxOciosas  int
    MaxVida    time.Duration
    MaxOcioso  time.Duration
}

type CacheConfig struct {
    URL string
    TTL time.Duration
}

type JWTConfig struct {
    Segredo   string
    Expiracao time.Duration
}

type LogConfig struct {
    Nivel   string
    Formato string
}

func Carregar(caminhoConfig string) (*Config, error) {
    v := viper.New()

    // Valores padrão — base de segurança
    v.SetDefault("servidor.host", "0.0.0.0")
    v.SetDefault("servidor.porta", 8080)
    v.SetDefault("servidor.read_timeout", "5s")
    v.SetDefault("servidor.write_timeout", "10s")
    v.SetDefault("servidor.idle_timeout", "60s")
    v.SetDefault("banco.max_conexoes", 25)
    v.SetDefault("banco.max_ociosas", 10)
    v.SetDefault("banco.max_vida", "5m")
    v.SetDefault("banco.max_ocioso", "2m")
    v.SetDefault("jwt.expiracao", "24h")
    v.SetDefault("log.nivel", "info")
    v.SetDefault("log.formato", "json")

    // Arquivo de configuração
    if caminhoConfig != "" {
        v.SetConfigFile(caminhoConfig)
    } else {
        v.AddConfigPath("./config")
        v.AddConfigPath(".")
        v.AddConfigPath("$HOME/.minha-app")
        v.SetConfigName("config")
        v.SetConfigType("yaml")
    }

    if err := v.ReadInConfig(); err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
            return nil, fmt.Errorf("ler configuração: %w", err)
        }
        // Arquivo não encontrado é aceitável — usamos padrões e env vars
    }

    // Variáveis de ambiente sobrescrevem o arquivo
    // APP_SERVIDOR_PORTA sobrescreve servidor.porta
    v.SetEnvPrefix("APP")
    v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    v.AutomaticEnv()

    return construirConfig(v)
}

func construirConfig(v *viper.Viper) (*Config, error) {
    readTimeout, err := time.ParseDuration(v.GetString("servidor.read_timeout"))
    if err != nil {
        return nil, fmt.Errorf("servidor.read_timeout inválido: %w", err)
    }

    writeTimeout, err := time.ParseDuration(v.GetString("servidor.write_timeout"))
    if err != nil {
        return nil, fmt.Errorf("servidor.write_timeout inválido: %w", err)
    }

    idleTimeout, err := time.ParseDuration(v.GetString("servidor.idle_timeout"))
    if err != nil {
        return nil, fmt.Errorf("servidor.idle_timeout inválido: %w", err)
    }

    maxVida, _ := time.ParseDuration(v.GetString("banco.max_vida"))
    maxOcioso, _ := time.ParseDuration(v.GetString("banco.max_ocioso"))
    cacheTTL, _ := time.ParseDuration(v.GetString("cache.ttl"))
    jwtExp, _ := time.ParseDuration(v.GetString("jwt.expiracao"))

    cfg := &Config{
        App: AppConfig{
            Nome:     v.GetString("app.nome"),
            Versao:   v.GetString("app.versao"),
            Ambiente: v.GetString("app.ambiente"),
        },
        Servidor: ServidorConfig{
            Host:         v.GetString("servidor.host"),
            Porta:        v.GetInt("servidor.porta"),
            ReadTimeout:  readTimeout,
            WriteTimeout: writeTimeout,
            IdleTimeout:  idleTimeout,
        },
        Banco: BancoConfig{
            URL:         v.GetString("banco.url"),
            MaxConexoes: v.GetInt("banco.max_conexoes"),
            MaxOciosas:  v.GetInt("banco.max_ociosas"),
            MaxVida:     maxVida,
            MaxOcioso:   maxOcioso,
        },
        Cache: CacheConfig{
            URL: v.GetString("cache.url"),
            TTL: cacheTTL,
        },
        JWT: JWTConfig{
            Segredo:   v.GetString("jwt.segredo"),
            Expiracao: jwtExp,
        },
        Log: LogConfig{
            Nivel:   v.GetString("log.nivel"),
            Formato: v.GetString("log.formato"),
        },
    }

    return cfg, validarConfig(cfg)
}

func validarConfig(cfg *Config) error {
    var erros []string

    if cfg.Banco.URL == "" {
        erros = append(erros, "banco.url é obrigatório")
    }

    if cfg.JWT.Segredo == "" {
        erros = append(erros, "jwt.segredo é obrigatório")
    }

    if cfg.Servidor.Porta < 1 || cfg.Servidor.Porta > 65535 {
        erros = append(erros, fmt.Sprintf("servidor.porta inválida: %d", cfg.Servidor.Porta))
    }

    if len(erros) > 0 {
        return fmt.Errorf("configuração inválida:\n  - %s",
            strings.Join(erros, "\n  - "))
    }

    return nil
}

Precedência do Viper

Maior precedência → Menor precedência

1. Set explícito (viper.Set)
2. Flag da linha de comando (via pflag/cobra)
3. Variável de ambiente
4. Arquivo de configuração
5. Valor padrão (viper.SetDefault)
// Demonstração de precedência
v := viper.New()
v.SetDefault("porta", 8080)         // 5. padrão
v.SetConfigFile("config.yaml")      // 4. arquivo (porta: 9090)
v.ReadInConfig()
v.AutomaticEnv()                     // 3. APP_PORTA=9191
// v.Set("porta", 9292)             // 1. mais alta precedência

fmt.Println(v.GetInt("porta")) // 9191 se APP_PORTA=9191, senão 9090

Watching: recarga automática de configuração

func configurarWatching(v *viper.Viper, onMudanca func(cfg *Config)) {
    v.WatchConfig()
    v.OnConfigChange(func(e fsnotify.Event) {
        fmt.Printf("Configuração alterada: %s\n", e.Name)

        cfg, err := construirConfig(v)
        if err != nil {
            fmt.Printf("Erro na nova configuração: %v\n", err)
            return
        }

        onMudanca(cfg)
    })
}

Integração com Cobra

package cmd

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

var (
    cfgFile string
    porta   int
)

var rootCmd = &cobra.Command{
    Use: "api",
    PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
        _, err := config.Carregar(cfgFile)
        return err
    },
}

func init() {
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
        "arquivo de configuração")
    rootCmd.PersistentFlags().IntVar(&porta, "port", 0,
        "porta do servidor (sobrescreve configuração)")

    // Vincula a flag ao Viper
    viper.BindPFlag("servidor.porta", rootCmd.PersistentFlags().Lookup("port"))
}

Validação de configuração com struct tags

Para projetos que preferem validação declarativa:

package main

import (
    "fmt"
    "os"
    "strconv"

    "github.com/go-playground/validator/v10"
)

type ConfigValidada struct {
    DatabaseURL  string        `validate:"required,url"`
    Porta        int           `validate:"required,min=1,max=65535"`
    JWTSegredo   string        `validate:"required,min=32"`
    Ambiente     string        `validate:"required,oneof=desenvolvimento staging producao"`
    LogNivel     string        `validate:"required,oneof=debug info warn error"`
}

func carregarEValidar() (*ConfigValidada, error) {
    porta, _ := strconv.Atoi(os.Getenv("PORTA"))
    if porta == 0 {
        porta = 8080
    }

    cfg := &ConfigValidada{
        DatabaseURL: os.Getenv("DATABASE_URL"),
        Porta:       porta,
        JWTSegredo:  os.Getenv("JWT_SEGREDO"),
        Ambiente:    getEnv("APP_ENV", "desenvolvimento"),
        LogNivel:    getEnv("LOG_LEVEL", "info"),
    }

    validate := validator.New()
    if err := validate.Struct(cfg); err != nil {
        erros := err.(validator.ValidationErrors)
        mensagens := make([]string, 0, len(erros))
        for _, e := range erros {
            mensagens = append(mensagens,
                fmt.Sprintf("  %s: falhou na validação '%s'", e.Field(), e.Tag()))
        }
        return nil, fmt.Errorf("configuração inválida:\n%s",
            strings.Join(mensagens, "\n"))
    }

    return cfg, nil
}

Exemplo completo: main.go com configuração

package main

import (
    "context"
    "fmt"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/joho/godotenv"
    "minha-api/config"
)

func main() {
    // 1. Carrega .env em desenvolvimento
    if os.Getenv("APP_ENV") == "" || os.Getenv("APP_ENV") == "desenvolvimento" {
        godotenv.Load()
    }

    // 2. Carrega configuração completa
    cfg, err := config.Carregar("")
    if err != nil {
        fmt.Fprintf(os.Stderr, "Configuração inválida: %v\n", err)
        os.Exit(1)
    }

    // 3. Configura logger
    var nivel slog.Level
    switch cfg.Log.Nivel {
    case "debug":
        nivel = slog.LevelDebug
    case "warn":
        nivel = slog.LevelWarn
    case "error":
        nivel = slog.LevelError
    default:
        nivel = slog.LevelInfo
    }

    handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: nivel,
    })
    logger := slog.New(handler).With(
        "serviço", cfg.App.Nome,
        "versao", cfg.App.Versao,
        "ambiente", cfg.App.Ambiente,
    )
    slog.SetDefault(logger)

    // 4. Log de inicialização
    slog.Info("aplicação iniciada",
        "porta", cfg.Servidor.Porta,
        "banco", mascararSenha(cfg.Banco.URL),
    )

    // 5. Inicia servidor
    mux := http.NewServeMux()
    mux.HandleFunc("GET /saude", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, `{"status":"ok","versao":"%s"}`, cfg.App.Versao)
    })

    addr := fmt.Sprintf("%s:%d", cfg.Servidor.Host, cfg.Servidor.Porta)
    servidor := &http.Server{
        Addr:         addr,
        Handler:      mux,
        ReadTimeout:  cfg.Servidor.ReadTimeout,
        WriteTimeout: cfg.Servidor.WriteTimeout,
        IdleTimeout:  cfg.Servidor.IdleTimeout,
    }

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        slog.Info("servidor HTTP iniciado", "addr", addr)
        if err := servidor.ListenAndServe(); err != http.ErrServerClosed {
            slog.Error("erro no servidor", "err", err)
            os.Exit(1)
        }
    }()

    <-quit
    slog.Info("sinal de encerramento recebido")

    ctx, cancelar := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancelar()

    if err := servidor.Shutdown(ctx); err != nil {
        slog.Error("erro no shutdown", "err", err)
        os.Exit(1)
    }

    slog.Info("servidor encerrado com sucesso")
}

func mascararSenha(url string) string {
    // Remove senha da URL para logging seguro
    // postgres://usuario:SENHA@host/banco → postgres://usuario:***@host/banco
    if idx := strings.Index(url, "@"); idx > 0 {
        inicio := strings.LastIndex(url[:idx], ":")
        if inicio > 0 {
            return url[:inicio+1] + "***" + url[idx:]
        }
    }
    return url
}

Resumo do que foi coberto

Este artigo apresentou configuração em Go de forma completa: os.Getenv e os.LookupEnv para casos simples, godotenv para arquivos .env em desenvolvimento, Viper com suporte a múltiplas fontes e precedência bem definida, arquivo de configuração YAML com valores tipados, variáveis de ambiente com prefixo automático, watching para recarga em tempo real, integração com Cobra, validação declarativa com struct tags e um exemplo completo de main.go com inicialização robusta. O próximo artigo explora linting e análise estática.


Referências e leituras complementares

  • Viper — documentação oficial — Referência completa de todos os recursos. https://github.com/spf13/viper

  • godotenv — Carregamento de arquivos .env para Go. https://github.com/joho/godotenv

  • The Twelve-Factor App — Config — Metodologia de configuração por variáveis de ambiente. https://12factor.net/config

  • go-playground/validator — Validação de structs com tags. https://github.com/go-playground/validator

  • envconfig — Alternativa ao Viper focada em variáveis de ambiente. https://github.com/kelseyhightower/envconfig

  • Documentação os.Getenv — Referência da biblioteca padrão. https://pkg.go.dev/os#Getenv


Próximo artigo: Artigo 41 — Linting, formatação e análise estática: gofmt, golangci-lint**


you asked

Sim


claude response

Comentários

Mais em Golang

Armadilhas da concorrência: race conditions e como detectá-las
Armadilhas da concorrência: race conditions e como detectá-las

Concorrência bem implementada torna sistemas mais rápidos, mais responsivos e...

O pacote context: propagação de cancelamento e prazos
O pacote context: propagação de cancelamento e prazos

Imagine um servidor HTTP recebendo uma requisição que desencadeia três operaç...

Operadores, expressões e conversão de tipo
Operadores, expressões e conversão de tipo

Se vari&aacute;veis s&atilde;o os substantivos de um programa, operadores s&a...