应使用 viper 统一管理配置,按环境变量 > .env 文件 > 配置文件 > 默认值优先级加载,显式绑定并校验敏感字段,全程用 []byte 处理密钥并立即覆盖擦除,禁止硬编码和提交 .env 文件。

用 viper 读取环境变量优先的配置,避免硬编码密钥
Go 原生 os.Getenv 只能读字符串,且无法 fallback 到文件或默认值,容易漏掉敏感字段校验。直接写 os.Getenv("DB_PASSWORD") 是危险起点——它不校验是否为空、不提示缺失、不支持类型转换。
推荐用 viper 统一入口,按优先级加载:环境变量 > .env 文件 > YAML/JSON 配置文件 > 默认值。关键点是把敏感字段显式声明为“必须存在”:
- 调用
viper.AutomaticEnv()启用环境变量前缀(如viper.SetEnvPrefix("APP"),则读APP_DB_PASSWORD) - 对每个敏感字段调用
viper.BindEnv("db.password", "APP_DB_PASSWORD"),再用viper.GetString("db.password")读取 - 启动时检查
viper.GetString("db.password") == ""并 panic,别等连接数据库时才报错
敏感字段必须从内存中擦除,不能只靠 defer 清空
读到的密码、token 等一旦进入 string 或 []byte,就可能被 GC 延迟回收,或留在内存 dump 中。Go 的 string 不可变,无法安全擦除。
正确做法是全程使用 []byte,并在使用后立即覆盖:
立即学习“go语言免费学习笔记(深入)”;
- 用
viper.GetRawBytes("db.password")直接获取字节切片(避免 string 转换) - 传给数据库驱动前,用
bytes.ReplaceAll(password, password, []byte{})或更稳妥的for i := range password { password[i] = 0 } - 注意:不要用
defer擦除——函数返回后才执行,期间仍可能被扫描;应在确认使用完毕后立刻擦除
禁止把 .env 提交到 Git,但本地开发需有模板
.env 文件若包含真实密钥,和明文密码文件没区别。很多团队误以为“.gitignore 了就安全”,却忘了 IDE、备份工具、日志打印可能意外泄露。
实际操作要分三层:
- Git 中只保留
.env.example,内容如APP_DB_PASSWORD=your_password_here,字段名完整但值占位 - CI/CD 流水线中,通过 secret manager 注入环境变量(如 GitHub Actions 的
secrets),完全跳过文件读取 - 本地开发时,运行前手动复制
.env.example为.env并填值——这个文件必须在.gitignore中,且建议加 pre-commit hook 检查是否误提交
用 config.go 封装初始化逻辑,别散落在 main 包里
把配置加载、校验、擦除逻辑堆在 main.go 会导致测试困难、复用率低、且容易漏掉擦除步骤。应该抽成独立包,比如 internal/config。
示例结构:
func Load() (*Config, error) {
v := viper.New()
v.SetConfigName("config")
v.AddConfigPath("/etc/myapp/")
v.AutomaticEnv()
v.BindEnv("db.password", "APP_DB_PASSWORD")
if err := v.ReadInConfig(); err != nil && !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return nil, err
}
cfg := &Config{
DBPassword: v.GetString("db.password"),
}
if cfg.DBPassword == "" {
return nil, errors.New("missing APP_DB_PASSWORD")
}
// 擦除原始 viper 缓存(viper 内部会缓存所有值)
v.Set("db.password", nil)
return cfg, nil
}
这样每次 Load() 都是干净上下文,也方便在测试中 mock viper 行为。
真正难的是让所有人遵守擦除习惯——哪怕只有一处忘记覆盖 []byte,整套机制就形同虚设。


















