Como carregar arquivos .env em Go usando apenas a Standard Library (sem dependências)
Muitas vezes adicionamos dependências externas como o godotenv por hábito, mas o Go oferece ferramentas poderosas na biblioteca padrão que tornam essa tarefa simples e educativa.
Criei um pacote minimalista chamado dotenv que resolve os principais problemas de parsing: suporte a export, remoção de comentários e tratamento de aspas.
1. O utilitário de limpeza (utils.go)
O segredo para um .env robusto é como tratamos o valor. Esta função lida com aspas e remove comentários in-line (ex: KEY="valor" # comentário).
package dotenv
import "strings"
func quotes(value string) string {
if len(value) == 0 {
return ""
}
// Se começar com aspas, busca o par correspondente
quote := value[0]
if quote == '"' || quote == '\'' {
content, _, found := strings.Cut(value[1:], string(quote))
if found {
return content
}
value = value[1:] // Caso não ache o par, remove a aspa inicial
}
// Remove comentários in-line e limpa espaços
value, _, _ = strings.Cut(value, "#")
return strings.TrimSpace(value)
}
2. A lógica principal (dotenv.go)
Aqui percorremos os arquivos e injetamos as variáveis no processo usando os.Setenv.
package dotenv
import (
"fmt"
"os"
"strings"
)
var FilenameVariables = []string{".env", ".env.local"}
func Collect() {
for _, filename := range FilenameVariables {
content, err := os.ReadFile(filename)
if err != nil {
continue // Pula se o arquivo não existir
}
for _, line := range strings.Split(string(content), "\n") {
line = strings.TrimSpace(line)
// Suporte a prefixo 'export ' (comum em shell scripts)
line = strings.TrimPrefix(line, "export ")
// Pula linhas vazias ou comentários
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Separa Chave=Valor de forma eficiente
key, value, found := strings.Cut(line, "=")
if !found {
continue
}
value = quotes(value)
os.Setenv(key, value)
// Opcional: Log para debug (recomendo remover em produção)
// fmt.Printf("Loaded: %s\n", key)
}
}
}
Por que usar strings.Cut?
Introduzido no Go 1.18, o strings.Cut é superior ao strings.Split para este caso porque:
- É mais rápido e aloca menos memória.
- Garante que você só divida na primeira ocorrência do
=, permitindo que o valor contenha outros símbolos de igual (ex: chaves de API).
Exemplo de uso
Basta chamar o Collect no início do seu main.go:
func main() {
dotenv.Collect()
apiKey := os.Getenv("API_KEY")
// ...
}
Conclusão:
Para projetos pequenos e médios, reduzir a árvore de dependências facilita a manutenção e a auditoria de segurança. O que você acha de implementar seus próprios utilitários em vez de inflar o go.mod?