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