← Voltar para o blog

Configuração em Go: Variáveis de Ambiente, Viper e Twelve-Factor

Configuração em Go na prática: variáveis de ambiente, envconfig, Viper com YAML, defaults, validação fail-fast, segredos e quando usar cada abordagem. Com exemplos de produção.

Todo serviço Go de verdade precisa de configuração: porta do servidor, URL do banco, timeout de requisição, nível de log, segredos de autenticação. A pergunta não é se você vai configurar a aplicação, mas onde essa configuração mora. Hardcoded no código fonte é o pior lugar; variáveis de ambiente e arquivos estruturados são o caminho profissional. Este guia mostra como tratar configuração em Go de forma idiomática — do os.Getenv ao Viper — seguindo o princípio twelve-factor, com defaults sensatos, validação fail-fast e segredos fora do repositório.

O texto complementa o guia de graceful shutdown em produção (que precisa saber por quanto tempo esperar antes de derrubar conexões), o guia de context, timeout e cancelamento (limites que vêm da configuração), o guia de logging estruturado com slog (nível de log é configuração) e o guia de health checks. Se você está montando uma API, leia também o guia de frameworks HTTP em Go e o guia de APIs REST.

O problema: configuração não é código

A armadilha clássica do iniciante é escrever const dbURL = "postgres://localhost:5432/meudb" direto no código. Isso funciona no seu notebook e quebra em produção, onde o banco tem outra URL, outra senha e outra porta. Pior: vira commit no Git, expondo segredos e travando o binário a um ambiente só.

O twelve-factor app, metodologia que orientou o design de aplicações modernas, é direto sobre isso: store config in the environment. Configuração é tudo que muda entre ambientes (desenvolvimento, staging, produção) — credenciais, hosts, portas, flags de feature. Código é tudo que não muda. Misturar os dois torna o deploy frágil e inseguro.

A consequência prática é que o mesmo binário compilado roda em qualquer ambiente, mudando apenas as variáveis de ambiente e os arquivos de configuração injetados. Isso é o que torna o deploy com Docker e o deploy no Kubernetes previsíveis: o artefato é imutável, a configuração é externa.

Variáveis de ambiente: a base

A forma mais simples e portátil de configurar um serviço Go é ler variáveis de ambiente com a biblioteca padrão. os.Getenv devolve uma string:

package main

import (
	"fmt"
	"os"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	fmt.Println("ouvindo na porta", port)
}

Há uma sutileza importante: os.Getenv devolve string vazia tanto quando a variável existe e está vazia quanto quando ela não existe. Para distinguir os dois casos, use os.LookupEnv:

port, ok := os.LookupEnv("PORT")
if !ok {
	port = "8080" // variável não definida: usa o padrão
}

Essa distinção importa quando uma variável vazia significa algo diferente de ausência (por exemplo, um campo de feature flag opcional). Para um ou dois valores, a biblioteca padrão basta. Conforme o número de configurações cresce, no entanto, três problemas aparecem: conversão manual de tipos (string para int, time.Duration, bool), ausência de defaults declarativos e erro silencioso quando uma variável obrigatória não está definida. É aí que entra o envconfig.

Envconfig: structs tipadas a partir do ambiente

O pacote github.com/kelseyhightower/envconfig resolve os três problemas de uma vez. Você declara uma struct e o envconfig a popula a partir das variáveis de ambiente, com defaults, tipos e validação de campos obrigatórios:

package config

import (
	"time"

	"github.com/kelseyhightower/envconfig"
)

type Config struct {
	Port           int           `envconfig:"PORT" default:"8080"`
	DatabaseURL    string        `envconfig:"DATABASE_URL" required:"true"`
	JWTSecret      string        `envconfig:"JWT_SECRET" required:"true"`
	RequestTimeout time.Duration `envconfig:"REQUEST_TIMEOUT" default:"30s"`
	MaxConnections int           `envconfig:"MAX_CONNECTIONS" default:"100"`
}

func Load() (Config, error) {
	var cfg Config
	if err := envconfig.Process("", &cfg); err != nil {
		return Config{}, err
	}
	return cfg, nil
}

A chamada envconfig.Process retorna erro automaticamente se um campo required:"true" não estiver definido — falha rápida e clara na inicialização, em vez de comportamento estranho minutos depois. O time.Duration é parseado a partir de strings como 30s ou 5m, e os int/bool são convertidos com segurança.

No main, carregue a configuração uma vez e falhe cedo:

func main() {
	cfg, err := config.Load()
	if err != nil {
		log.Fatalf("configuração inválida: %v", err)
	}
	// ... passa cfg para o resto da aplicação
}

Essa abordagem casa perfeitamente com clean architecture sem overengineering: a Config é um valor de domínio injetado onde precisa, sem globals espalhados. Para muitos serviços, o envconfig é tudo que você precisa.

Viper: o canivete suíço da configuração

Quando a configuração precisa vir de várias fontes — um arquivo YAML, variáveis de ambiente, flags de linha de comando e defaults de código — o Viper é a ferramenta padrão do ecossistema. Ele lê configuração em camadas, com precedência clara: a última definição vence.

func Load() (*Config, error) {
	v := viper.New()

	v.SetConfigName("config")
	v.SetConfigType("yaml")
	v.AddConfigPath(".")
	v.AddConfigPath("/etc/myapp")

	v.SetEnvPrefix("MYAPP")
	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	v.AutomaticEnv()

	v.SetDefault("server.port", 8080)
	v.SetDefault("server.shutdownTimeout", "30s")
	v.SetDefault("database.maxConns", 25)

	if err := v.ReadInConfig(); err != nil {
		return nil, fmt.Errorf("ler config: %w", err)
	}

	var cfg Config
	if err := v.Unmarshal(&cfg); err != nil {
		return nil, fmt.Errorf("unmarshal config: %w", err)
	}
	return &cfg, nil
}

O v.Unmarshal(&cfg) popula uma struct tipada a partir do mapa interno do Viper, com mapeamento automático entre as chaves do YAML e os campos da struct via tags ou nomes. A Config fica organizada por seção:

type Config struct {
	Server   ServerConfig   `mapstructure:"server"`
	Database DatabaseConfig `mapstructure:"database"`
}

type ServerConfig struct {
	Port            int           `mapstructure:"port"`
	ShutdownTimeout time.Duration `mapstructure:"shutdownTimeout"`
}

type DatabaseConfig struct {
	URL      string `mapstructure:"url"`
	MaxConns int    `mapstructure:"maxConns"`
}

O arquivo config.yaml correspondente:

server:
  port: 8080
  shutdownTimeout: 30s
database:
  url: "postgres://user:pass@localhost:5432/myapp?sslmode=disable"
  maxConns: 25

A combinação de AutomaticEnv com SetEnvPrefix permite que a variável MYAPP_SERVER_PORT sobrescreva server.port do YAML. Assim, você versiona defaults razoáveis no arquivo e injeta o que muda por ambiente — exatamente o que o deploy em containers espera. O SetEnvKeyReplacer é o detalhe que conecta o ponto do YAML (server.port) ao underscore da variável (SERVER_PORT).

Defaults, validação e fail-fast

Defaults bons são o que mantém um desenvolvedor novo rodando o projeto em minutos, mas eles têm um limite: defaults servem para desenvolvimento, não para segurança em produção. Um segredo nunca deve ter default. A regra prática é separar campos em três grupos:

Tipo de campoExemploDefaultObrigatório
Operacionalporta, timeout, poolSimNão
ConexãoURL de bancoEm devSim em prod
SegredoJWT secret, API keyNuncaSim

Depois de carregar a configuração, valide a consistência antes de subir o servidor. O Viper não valida regras de negócio; faça isso em código explícito:

func (c *Config) Validate() error {
	if c.Server.Port < 1 || c.Server.Port > 65535 {
		return errors.New("server.port deve estar entre 1 e 65535")
	}
	if c.Database.URL == "" {
		return errors.New("database.url é obrigatório")
	}
	if c.Database.MaxConns < 1 {
		return errors.New("database.maxConns deve ser positivo")
	}
	return nil
}

Falhar na inicialização é infinitamente melhor do que falhar no meio de uma requisição. Combine essa validação com logging estruturado via slog para registrar a configuração carregada (menos os segredos) e facilitar o diagnóstico.

Configuração por ambiente

A forma mais limpa de lidar com múltiplos ambientes não é ter vários binários, mas sim um binário e várias fontes de configuração. Um padrão comum é ter um config.yaml com defaults versionado no repositório e sobrescrever via variáveis de ambiente o que for específico de cada ambiente:

# Desenvolvimento
DATABASE_URL=postgres://localhost:5432/myapp_dev LOG_LEVEL=debug ./myapp

# Produção
DATABASE_URL=postgres://prod-host:5432/myapp LOG_LEVEL=info ./myapp

Evite a tentação de versionar config.prod.yaml no Git com credenciais reais — é a origem mais comum de vazamentos. Se precisar de arquivos por ambiente, injete-os pelo orquestrador (ConfigMaps do Kubernetes, por exemplo) e nunca os commite.

Segredos: o que nunca vai no config

A linha que separa configuração de segredo é simples: se vazaria em um log, é segredo. Senhas de banco, chaves de API, tokens JWT e credenciais de provedores de nuvem são segredos. Eles não devem ir em arquivos versionados nem em logs.

Em produção, a boa prática é injetar segredos pelo orquestrador: Secrets do Kubernetes, AWS Secrets Manager, Google Secret Manager ou HashiCorp Vault. Todos entregam segredos como variáveis de ambiente ou arquivos efêmeros no container, com rotação controlada e auditoria. A aplicação Go lê do ambiente do mesmo jeito que lê qualquer outra variável — a diferença está na origem segura.

// Lendo um segredo injetado como variável de ambiente pelo orquestrador
jwtSecret := os.Getenv("JWT_SECRET")
if jwtSecret == "" {
	log.Fatal("JWT_SECRET não definido — verifique o Secrets do namespace")
}

Para autenticação que depende desses segredos, veja o guia de autenticação e autorização em APIs Go. E nunca, nunca mesmo, coloque um segredo de verdade em um exemplo de código ou em um comentário — o Git lembra de tudo.

Recarregamento a quente

O Viper consegue detectar mudanças no arquivo de configuração em tempo de execução e atualizar a cópia em memória:

v.OnConfigChange(func(e fsnotify.Event) {
	log.Info("configuração recarregada", "file", e.Name)
	// reponderar a struct e reaplicar onde for necessário
})
v.WatchConfig()

Isso é útil para mexer em nível de log ou em limites operacionais sem reiniciar. Mas há uma pegadinha importante: o recarregamento só atualiza a leitura do arquivo. Conexões de banco abertas, pools e clientes HTTP não se recriam sozinhos. Mudanças estruturais (trocar a URL do banco, por exemplo) exigem reaplicar a configuração no objeto que a usa ou, mais seguro, reiniciar o serviço de forma controlada — o que leva direto ao graceful shutdown.

Para a maioria dos serviços, a complexidade do recarregamento a quente não compensa: prefira reiniciar o container em um deploy normal. Reserve o hot reload para casos em que a reconfiguração sem downtime traz valor real, como ajustar limites de rate limiting ou nível de log sob carga.

Viper ou envconfig: qual usar

A escolha depende do número de fontes de configuração e da complexidade do serviço:

CritérioenvconfigViper
Origem da configuraçãoSó variáveis de ambienteYAML, JSON, env, flags, etcd
Defaults declarativosSim, via tagsSim, via SetDefault
Tipagem forteSim (struct + tags)Sim (via Unmarshal)
Recarregamento a quenteNãoSim (WatchConfig)
Código e dependênciasMínimosMais pesado
Boa para microservices simplesSimAceitável
Boa para apps com muito perfilCom esforçoSim

A recomendação prática: comece com envconfig para serviços que vivem de variáveis de ambiente (o caso típico em containers e serverless como AWS Lambda). Migre para Viper quando precisar combinar arquivos estruturados com env, suportar múltiplos ambientes via arquivos ou habilitar hot reload. Não existe resposta universal — existe a ferramenta certa para o seu nível de complexidade.

Armadilhas comuns

Três erros aparecem com frequência em configuração de serviços Go:

  1. Segredo no Git — qualquer credencial real em config.yaml versionado é um vazamento esperando para acontecer. Use o histórico do Git para auditar: git log -p -- config.yaml não deve conter senhas.
  2. Default em campo obrigatório — quando um segredo tem default “para facilitar”, produção acaba rodando com um valor fraco. Campos de segredo devem ser required:"true" sem exceção.
  3. Configuração espalhada — ler os.Getenv em dezenas de pontos do código cria um sistema impossível de auditar. Centralize em uma struct carregada na inicialização e passada explicitamente.

Para diagnosticar problemas de configuração em produção, registre a configuração carregada (com segredos mascarados) via slog e monitore variáveis não definidas que caíram em defaults perigosos. Configuração é parte da observabilidade do serviço.

Se você trabalha em stacks múltiplas, vale comparar com o jeito que o Python Dev Brasil trata configuração tipada com Pydantic Settings: a ideia de validar e tipar configuração a partir do ambiente é a mesma, só muda a linguagem.

Perguntas frequentes

Como ler variáveis de ambiente em Go?

Use os.Getenv("NOME") para ler uma variável como string, ou os.LookupEnv("NOME") quando você precisa distinguir uma variável vazia de uma variável não definida. Defina um valor padrão quando a leitura indicar que a variável não existe. Para validar tipos e defaults de forma automática, prefira o envconfig, que popula uma struct a partir das variáveis de ambiente com tags declarativas.

Viper ou envconfig: qual usar?

Use envconfig quando a configuração vem só de variáveis de ambiente e você quer simplicidade e tipagem forte com pouco código. Use Viper quando você precisa combinar arquivos YAML/JSON, variáveis de ambiente, flags de linha de comando, defaults e recarregamento a quente na mesma aplicação. Muitos serviços só precisam de envconfig; o Viper vale a pena quando há várias fontes de configuração combinadas.

Onde guardar segredos como senhas e tokens em Go?

Segredos não devem ir em arquivos de configuração versionados no Git. Em produção, injete segredos como variáveis de ambiente via orquestrador: Secrets do Kubernetes, AWS Secrets Manager, Google Secret Manager ou HashiCorp Vault. A aplicação Go lê do ambiente da mesma forma que lê qualquer outra configuração; a diferença é a origem controlada, rotacionável e auditável.

O Viper faz recarregamento a quente de configuração?

Sim, com WatchConfig e OnConfigChange o Viper detecta mudanças no arquivo de configuração e atualiza a cópia em memória. Mas conexões de banco, clientes HTTP e pools não se recriam sozinhos: você precisa reponderar as mudanças e reaplicá-las, eventualmente reiniciando o serviço de forma controlada com graceful shutdown.

O que é o princípio twelve-factor aplicado a configuração?

O twelve-factor determina que configuração deve ficar separada do código, injetada pelo ambiente em que a aplicação roda. Assim, o mesmo binário roda idêntico em desenvolvimento, staging e produção, mudando apenas as variáveis de ambiente e os segredos. Isso facilita deploy, testes e portabilidade entre provedores de nuvem.

Conclusão

Configuração bem feita é invisível: a aplicação sobe em qualquer ambiente com as variáveis certas, falha cedo quando algo obrigatório falta e nunca expõe segredos. Comece com a biblioteca padrão para casos simples, adote envconfig para tipagem e defaults declarativos, e recorra ao Viper quando precisar combinar múltiplas fontes de configuração. Em todos os casos, mantenha a regra do twelve-factor: configuração fora do código, segredos fora do Git, e um único binário que roda em qualquer lugar.

Para aprofundar, explore o roadmap de Go para 2026, confira vagas de Go no Brasil e entenda a faixa de salários de desenvolvedor Go no mercado brasileiro. O próximo passo natural depois de dominar configuração é arquitetar a aplicação de ponta a ponta — leia o guia de Go para backend e o de clean architecture sem overengineering para amarrar configuração, módulos e casos de uso sem cair em complexidade desnecessária.