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

Exercícios

  1. Escreva um programa em Go que leia uma variável de ambiente chamada GREETING e, 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)
    }
    
  2. Crie um programa que aceite uma flag -name (string) e uma flag -age (int) usando o pacote flag. Se a flag -name nã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)
    }
    
  3. 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)
    }
    
  4. Usando a biblioteca viper, escreva um programa que leia uma configuração de um arquivo YAML chamado config.yaml com as chaves server.port e database.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)
    }
    
  5. Implemente uma função validateConfig que recebe um mapa map[string]string e 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.