Configuração de aplicações
LidoEsta aula explora as principais formas de configurar aplicações em Go: variáveis de ambiente, flags de linha de comando e arquivos de configuração. Você aprenderá a usar a biblioteca padrão e bibliotecas populares, além de boas práticas para manter a configuração organizada e segura.
Em qualquer aplicação real, precisamos de uma forma de configurar comportamentos sem recompilar o código. Isso pode incluir credenciais de banco de dados, URLs de serviços externos, portas de servidor, níveis de log, entre outros. Em Go, temos várias abordagens para lidar com configuração, cada uma com seus prós e contras. Nesta aula, vamos explorar as três mais comuns: variáveis de ambiente, flags de linha de comando e arquivos de configuração. Também discutiremos boas práticas para manter sua configuração limpa, segura e fácil de gerenciar.
Vamos começar entendendo o papel de cada método e depois mergulhar em exemplos práticos que você pode usar no seu dia a dia como desenvolvedor Go.
Variáveis de ambiente
Variáveis de ambiente são valores definidos fora do programa, no ambiente do sistema operacional. Elas são amplamente usadas para configuração, especialmente em ambientes de nuvem e contêineres, pois permitem que a mesma imagem de aplicação seja executada em diferentes ambientes (dev, staging, produção) apenas alterando as variáveis. Em Go, você pode acessá-las com a função os.Getenv que retorna o valor como string, ou os.LookupEnv que também informa se a variável está definida.
Uma vantagem das variáveis de ambiente é que elas não são versionadas no código, o que ajuda a manter segredos fora do repositório. No entanto, é importante definir valores padrão para evitar que a aplicação quebre quando uma variável não estiver definida. Vamos ver um exemplo prático.
package main
import (
"fmt"
"os"
)
func main() {
// Exemplo 1: GetEnv simples
port := os.Getenv("PORT")
if port == "" {
port = "8080" // valor padrão
}
fmt.Println("Porta:", port)
// Exemplo 2: LookupEnv para saber se existe
dbURL, exists := os.LookupEnv("DATABASE_URL")
if !exists {
fmt.Println("DATABASE_URL não definida, usando valor padrão")
dbURL = "postgres://localhost:5432/mydb"
}
fmt.Println("DB URL:", dbURL)
}
No exemplo acima, usamos os.Getenv para ler a variável PORT e, se não estiver definida, usamos um padrão. Com os.LookupEnv podemos verificar se a variável existe, o que é útil para diferenciar entre variável vazia e não definida. Para variáveis booleanas ou numéricas, você precisará fazer a conversão manualmente, mas existem bibliotecas que facilitam isso, como github.com/caarlos0/env que permite mapear variáveis para structs automaticamente.
Flags
Flags de linha de comando são outra forma comum de configuração, especialmente para ferramentas CLI e servidores. Elas permitem que o usuário especifique opções no momento da execução, como myapp -port=9090 -debug. A biblioteca padrão flag oferece funções para definir e analisar flags. Para flags mais avançadas, como suporte a subcomandos ou flags com valores padrão mais complexos, você pode usar bibliotecas como spf13/pflag ou urfave/cli.
Vamos ver um exemplo usando a biblioteca padrão flag. Definimos uma flag port do tipo int, uma flag debug do tipo bool, e uma flag name do tipo string. Depois chamamos flag.Parse() para processar os argumentos da linha de comando.
package main
import (
"flag"
"fmt"
)
func main() {
// Definindo flags
port := flag.Int("port", 8080, "Porta do servidor")
debug := flag.Bool("debug", false, "Habilita modo debug")
name := flag.String("name", "world", "Nome para saudação")
// Parse dos argumentos
flag.Parse()
fmt.Printf("Porta: %d\n", *port)
fmt.Printf("Debug: %v\n", *debug)
fmt.Printf("Olá, %s!\n", *name)
}
Executando go run main.go -port=9090 -debug -name=Gopher, a saída será:
Porta: 9090
Debug: true
Olá, Gopher!
Uma boa prática com flags é fornecer valores padrão sensatos e documentar cada flag com uma descrição clara, que aparecerá no help automático (-h). Para flags mais complexas, como listas ou mapas, você pode implementar a interface flag.Value ou usar bibliotecas externas.
Arquivos de config
Arquivos de configuração são úteis quando você tem muitas opções ou deseja que a configuração seja versionada (não segredos). Formatos comuns incluem JSON, YAML, TOML e até mesmo arquivos .env. Em Go, você pode ler esses arquivos usando a biblioteca padrão (encoding/json, gopkg.in/yaml.v3, github.com/BurntSushi/toml) ou bibliotecas de gerenciamento de configuração como viper, que suporta múltiplos formatos e fontes (arquivo, ambiente, flags) com precedência definida.
Vamos ver um exemplo simples com JSON. Suponha que temos um arquivo config.json com o seguinte conteúdo:
{
"server": {
"port": 8080,
"timeout": 30
},
"database": {
"url": "postgres://localhost:5432/mydb",
"maxConnections": 10
}
}
Podemos definir structs que espelham essa estrutura e usar json.Unmarshal para popular os dados. Vamos criar um programa que lê esse arquivo e imprime os valores.
package main
import (
"encoding/json"
"fmt"
"os"
)
type Config struct {
Server struct {
Port int `json:"port"`
Timeout int `json:"timeout"`
} `json:"server"`
Database struct {
URL string `json:"url"`
MaxConnections int `json:"maxConnections"`
} `json:"database"`
}
func main() {
// Abrir arquivo
file, err := os.Open("config.json")
if err != nil {
panic(err)
}
defer file.Close()
// Decodificar JSON
var config Config
decoder := json.NewDecoder(file)
err = decoder.Decode(&config)
if err != nil {
panic(err)
}
fmt.Printf("Porta: %d\n", config.Server.Port)
fmt.Printf("Timeout: %d\n", config.Server.Timeout)
fmt.Printf("DB URL: %s\n", config.Database.URL)
fmt.Printf("Max Connections: %d\n", config.Database.MaxConnections)
}
Para YAML ou TOML, o processo é similar, mas você precisa importar a biblioteca correspondente. O pacote viper é muito popular porque unifica o acesso a várias fontes e formatos, permitindo que você defina uma ordem de precedência (por exemplo, flags > variáveis de ambiente > arquivo de config). Isso é extremamente útil em aplicações que precisam de flexibilidade.
Boas práticas
Ao configurar aplicações, é importante seguir algumas boas práticas para manter o código limpo e seguro. Primeiro, nunca inclua segredos (senhas, chaves de API) diretamente no código ou em arquivos versionados. Use variáveis de ambiente ou sistemas de gerenciamento de segredos (como Vault). Segundo, forneça valores padrão sensatos para que a aplicação possa ser executada sem configuração adicional em ambientes de desenvolvimento. Terceiro, valide as configurações no início da execução, retornando erros claros se algo estiver faltando ou inválido.
Outra boa prática é usar uma estrutura de configuração centralizada, como uma struct Config, que pode ser preenchida a partir de múltiplas fontes. Isso facilita testes, pois você pode injetar configurações diferentes. Além disso, documente cada opção de configuração, seja em comentários no código, no help das flags ou em exemplos de arquivos de config. Por fim, considere usar bibliotecas como viper para gerenciar configuração de forma robusta, mas não exagere: para aplicações simples, a biblioteca padrão pode ser suficiente.
Vamos ver um exemplo de validação de configuração usando uma struct e um método Validate:
type Config struct {
Port int
DB struct {
URL string
}
}
func (c Config) Validate() error {
if c.Port == 0 {
return errors.New("port não pode ser zero")
}
if c.DB.URL == "" {
return errors.New("database url é obrigatória")
}
return nil
}
No main, após carregar a configuração, chamamos Validate() e tratamos o erro. Isso evita que a aplicação rode com configuração inválida.
Referências
- Documentação oficial do pacote os - Getenv
- Documentação oficial do pacote flag
- Documentação oficial do pacote encoding/json
- Viper - gerenciamento de configuração para Go
- env - biblioteca para parse de variáveis de ambiente em structs
- Pacote yaml.v3 para Go
- Pacote toml para Go
Exercícios
-
Escreva um programa em Go que leia uma variável de ambiente chamada
GREETINGe, se não estiver definida, use "Olá" como padrão. Imprima a saudação no console.✓ Resposta:package main import ( "fmt" "os" ) func main() { greeting := os.Getenv("GREETING") if greeting == "" { greeting = "Olá" } fmt.Println(greeting) } -
Crie um programa que aceite uma flag
-name(string) e uma flag-age(int) usando o pacoteflag. Se a flag-namenão for fornecida, use "Visitante". Imprima os valores.✓ Resposta:package main import ( "flag" "fmt" ) func main() { name := flag.String("name", "Visitante", "Nome do usuário") age := flag.Int("age", 0, "Idade do usuário") flag.Parse() fmt.Printf("Nome: %s\n", *name) fmt.Printf("Idade: %d\n", *age) } -
Dado o seguinte JSON em um arquivo
config.json:{"app": {"name": "MyApp", "version": "1.0"}}, escreva um programa que leia o arquivo e imprima o nome e a versão.✓ Resposta:package main import ( "encoding/json" "fmt" "os" ) type Config struct { App struct { Name string `json:"name"` Version string `json:"version"` } `json:"app"` } func main() { file, err := os.Open("config.json") if err != nil { panic(err) } defer file.Close() var config Config if err := json.NewDecoder(file).Decode(&config); err != nil { panic(err) } fmt.Printf("Nome: %s, Versão: %s\n", config.App.Name, config.App.Version) } -
Usando a biblioteca
viper, escreva um programa que leia uma configuração de um arquivo YAML chamadoconfig.yamlcom as chavesserver.portedatabase.host. Imprima os valores.✓ Resposta:package main import ( "fmt" "github.com/spf13/viper" ) func main() { viper.SetConfigName("config") // nome do arquivo sem extensão viper.SetConfigType("yaml") viper.AddConfigPath(".") // diretório atual if err := viper.ReadInConfig(); err != nil { panic(err) } port := viper.GetInt("server.port") host := viper.GetString("database.host") fmt.Printf("Porta: %d, Host: %s\n", port, host) } -
Implemente uma função
validateConfigque recebe um mapamap[string]stringe verifica se as chaves obrigatórias "HOST" e "PORT" estão presentes e não vazias. Retorne um erro se alguma estiver ausente ou vazia.✓ Resposta:package main import ( "errors" "fmt" ) func validateConfig(cfg map[string]string) error { if cfg["HOST"] == "" { return errors.New("HOST é obrigatório") } if cfg["PORT"] == "" { return errors.New("PORT é obrigatório") } return nil } func main() { cfg := map[string]string{"HOST": "localhost", "PORT": "8080"} if err := validateConfig(cfg); err != nil { fmt.Println("Erro:", err) } else { fmt.Println("Config válida") } }
Observações finais
Nesta aula, cobrimos as principais maneiras de configurar aplicações em Go. Lembre-se de que a escolha do método depende do seu caso de uso: para ferramentas CLI, flags são ideais; para serviços em contêineres, variáveis de ambiente são comuns; para configurações complexas, arquivos de config são melhores. Boas práticas incluem usar valores padrão, validar configurações e nunca versionar segredos. Com essas ferramentas, você pode construir aplicações flexíveis e fáceis de operar.