2. Go 环境变量管理:os.Getenv、os.LookupEnv、.env 文件与 godotenv 库的使用

环境变量是 Go 项目多环境配置的基石。本章深入剖析 Go 标准库提供的环境变量操作函数,并引入 .env 文件与 godotenv 库,帮助你在开发、测试、生产等不同环境间无缝切换配置。

2.1 标准库环境变量操作

Go 标准库 os 包提供了两个核心函数用于读取环境变量:os.Getenv 和 os.LookupEnv。理解它们的差异是正确使用环境变量的前提。

函数 签名 返回值 适用场景
os.Getenv func Getenv(key string) string 返回 key 对应的值;若不存在则返回空字符串 快速读取,不关心变量是否存在
os.LookupEnv func LookupEnv(key string) (string, bool) 返回 (值, 是否存在) 二元组 需要区分“变量存在但值为空”与“变量不存在”

关键区别:os.Getenv 无法区分“变量未设置”和“变量设置为空字符串”两种情况。在配置校验场景中,os.LookupEnv 是更安全的选择。

2.1.1 基础使用示例

package main

import (
    "fmt"
    "os"
)

func main() {
    // 使用 os.Getenv 快速读取
    dbHost := os.Getenv("DB_HOST")
    fmt.Printf("DB_HOST: %s\n", dbHost)

    // 使用 os.LookupEnv 安全读取
    dbPort, exists := os.LookupEnv("DB_PORT")
    if !exists {
        fmt.Println("DB_PORT 未设置,使用默认值 5432")
        dbPort = "5432"
    }
    fmt.Printf("DB_PORT: %s\n", dbPort)
}

2.2 .env 文件与 godotenv 库

直接依赖系统环境变量在开发阶段存在明显痛点:每个开发者需要手动设置大量变量,且无法在代码仓库中共享默认配置。.env 文件配合 godotenv 库完美解决了这一问题。

2.2.1 .env 文件规范

.env 文件是一个纯文本文件,每行定义一个环境变量,格式为 KEY=VALUE。支持注释(以 # 开头)和空行。

# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_USER=app_user
DB_PASSWORD=dev_password

# Redis 配置
REDIS_ADDR=127.0.0.1:6379
REDIS_PASSWORD=

# 应用配置
APP_ENV=development
LOG_LEVEL=debug

最佳实践:将 .env 文件添加到 .gitignore 中,避免敏感信息泄露。同时提供 .env.example 作为模板提交到仓库,供其他开发者参考。

2.2.2 godotenv 库的使用

godotenv 是 Go 生态中最流行的 .env 文件加载库。它会在程序启动时读取 .env 文件,并将其中定义的变量注入到当前进程的环境变量中。

安装:

go get github.com/joho/godotenv

基本用法:

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/joho/godotenv"
)

func main() {
    // 加载 .env 文件
    err := godotenv.Load()
    if err != nil {
        log.Fatal("加载 .env 文件失败:", err)
    }

    // 之后就可以像使用普通环境变量一样读取
    dbHost := os.Getenv("DB_HOST")
    dbPort := os.Getenv("DB_PORT")
    fmt.Printf("连接数据库: %s:%s\n", dbHost, dbPort)
}

2.3 环境变量加载优先级与生命周期

理解环境变量的加载顺序和生命周期,是避免配置混乱的关键。

环境变量加载优先级与生命周期 系统环境变量 export/set 命令设置 .env 文件变量 godotenv.Load() 加载 进程环境变量 os.Getenv 读取 生命周期规则 1. 系统环境变量在进程启动前已存在,优先级最高 2. godotenv.Load() 在程序启动时加载 .env 文件,覆盖同名系统变量 3. 加载后的变量仅对当前进程及其子进程可见,进程退出后自动销毁 覆盖规则(重要) • 如果系统环境变量已存在,godotenv 默认不会覆盖它 • 使用 godotenv.Load(".env", ".env.local") 时,后加载的文件会覆盖先加载的 • 可通过 godotenv.Overload() 强制覆盖系统环境变量(谨慎使用)

2.4 多环境 .env 文件策略

在实际项目中,通常需要为不同环境维护不同的 .env 文件。以下是推荐的命名规范和加载策略。

文件名 用途 是否提交到 Git 加载优先级
.env 默认配置,所有环境共享的基础值 是(仅包含非敏感默认值) 低
.env.development 开发环境专用配置 是 中
.env.production 生产环境专用配置 否(包含敏感信息) 中
.env.local 本地覆盖配置,每个开发者可自定义 否 高

2.4.1 多文件加载实现

package main

import (
    "fmt"
    "os"

    "github.com/joho/godotenv"
)

func loadEnvConfig() {
    // 获取当前环境,默认为 development
    env := os.Getenv("APP_ENV")
    if env == "" {
        env = "development"
    }

    // 按优先级从低到高加载
    // 1. 基础配置
    godotenv.Load(".env")
    // 2. 环境特定配置
    godotenv.Load(fmt.Sprintf(".env.%s", env))
    // 3. 本地覆盖配置(最高优先级)
    godotenv.Load(".env.local")

    // 注意:后加载的文件会覆盖先加载的相同变量
}

func main() {
    loadEnvConfig()

    // 读取配置
    dbHost := os.Getenv("DB_HOST")
    dbPort := os.Getenv("DB_PORT")
    fmt.Printf("当前环境: %s\n", os.Getenv("APP_ENV"))
    fmt.Printf("数据库地址: %s:%s\n", dbHost, dbPort)
}

安全警告:永远不要将包含真实密码、API Key、Token 等敏感信息的 .env 文件提交到 Git 仓库。使用 .env.example 作为模板,并在 .gitignore 中明确排除 .env、.env.local、.env.production 等文件。

2.5 环境变量校验与默认值

在生产环境中,环境变量缺失或格式错误可能导致运行时异常。建议在应用启动时进行集中校验。

package config

import (
    "fmt"
    "os"
    "strconv"
)

// Config 应用配置结构体
type Config struct {
    DBHost     string
    DBPort     int
    DBUser     string
    DBPassword string
    RedisAddr  string
    LogLevel   string
}

// LoadConfig 加载并校验配置
func LoadConfig() (*Config, error) {
    cfg := &Config{}

    // 字符串类型,带默认值
    cfg.DBHost = getEnv("DB_HOST", "localhost")
    cfg.DBUser = getEnv("DB_USER", "app")
    cfg.RedisAddr = getEnv("REDIS_ADDR", "127.0.0.1:6379")
    cfg.LogLevel = getEnv("LOG_LEVEL", "info")

    // 整数类型,需要转换
    portStr := getEnv("DB_PORT", "5432")
    port, err := strconv.Atoi(portStr)
    if err != nil {
        return nil, fmt.Errorf("DB_PORT 格式错误: %s", portStr)
    }
    cfg.DBPort = port

    // 必填字段校验
    cfg.DBPassword = os.Getenv("DB_PASSWORD")
    if cfg.DBPassword == "" {
        return nil, fmt.Errorf("DB_PASSWORD 是必填字段")
    }

    return cfg, nil
}

// getEnv 读取环境变量,若不存在则返回默认值
func getEnv(key, defaultVal string) string {
    if val, exists := os.LookupEnv(key); exists {
        return val
    }
    return defaultVal
}

2.6 实战:构建可扩展的环境变量加载器

将上述知识整合为一个可复用的环境变量加载器,支持多文件加载、变量覆盖和配置校验。

环境变量加载器架构 LoadConfig() 加载 .env 文件链 .env → .env.{env} → .env.local 校验必填字段 类型转换 + 存在性检查 *Config / error

生产建议:在 CI/CD 流水线中,建议将敏感环境变量通过平台的安全机制(如 GitHub Secrets、GitLab CI Variables、Kubernetes Secrets)注入,而非依赖 .env 文件。仅在本地开发时使用 .env 文件提升效率。

通过本章的学习,你已经掌握了 Go 环境变量管理的完整知识体系:从标准库的基础操作,到 .env 文件与 godotenv 库的进阶使用,再到多环境策略和配置校验。这些技能将为后续的依赖注入和领域边界适配打下坚实基础。