配置管理是后端项目里很基础但容易混乱的部分。服务端程序通常需要数据库地址、Redis 地址、日志级别、HTTP 端口、第三方接口密钥等配置。如果这些内容散落在代码里,部署到测试、预发和生产环境时就会变得难以维护。
本文介绍一种简单、可落地的 Go 配置管理方式:默认值放代码,环境相关配置通过配置文件或环境变量覆盖,敏感信息优先使用环境变量。
配置应该解决的问题 一个可维护的配置方案至少要满足以下要求:
本地开发可以快速启动。
测试、预发、生产环境可以使用不同配置。
敏感信息不提交到 Git。
程序启动时能尽早发现缺失或非法配置。
配置结构和业务模块对应,便于理解。
不要把配置系统做得过重。对于中小型服务,结构体加 JSON、YAML 或环境变量已经足够。
定义配置结构体 先定义一个结构体描述服务需要的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Config struct { App AppConfig `json:"app"` HTTP HTTPConfig `json:"http"` Database DatabaseConfig `json:"database"` } type AppConfig struct { Env string `json:"env"` LogLevel string `json:"logLevel"` } type HTTPConfig struct { Addr string `json:"addr"` ReadTimeout int `json:"readTimeout"` WriteTimeout int `json:"writeTimeout"` } type DatabaseConfig struct { DSN string `json:"dsn"` MaxOpenConns int `json:"maxOpenConns"` MaxIdleConns int `json:"maxIdleConns"` ConnMaxLifetime int `json:"connMaxLifetime"` }
结构体的好处是配置项清晰,IDE 可以提示字段,后续校验也方便。
提供默认配置 默认配置适合放非敏感、通用的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func DefaultConfig () Config { return Config{ App: AppConfig{ Env: "local" , LogLevel: "debug" , }, HTTP: HTTPConfig{ Addr: ":8080" , ReadTimeout: 5 , WriteTimeout: 10 , }, Database: DatabaseConfig{ MaxOpenConns: 20 , MaxIdleConns: 5 , ConnMaxLifetime: 300 , }, } }
这样本地启动时即使缺少部分配置,也有合理默认值。但数据库密码、API token 这类敏感内容不应该提供默认值。
从 JSON 文件加载配置 为了减少依赖,可以先用标准库 JSON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func LoadConfig (path string ) (Config, error ) { cfg := DefaultConfig() if path == "" { applyEnv(&cfg) return cfg, validate(cfg) } data, err := os.ReadFile(path) if err != nil { return cfg, fmt.Errorf("read config: %w" , err) } if err := json.Unmarshal(data, &cfg); err != nil { return cfg, fmt.Errorf("parse config: %w" , err) } applyEnv(&cfg) return cfg, validate(cfg) }
示例配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "app" : { "env" : "dev" , "logLevel" : "info" } , "http" : { "addr" : ":8080" , "readTimeout" : 5 , "writeTimeout" : 10 } , "database" : { "dsn" : "user:password@tcp(localhost:3306)/demo?parseTime=true" , "maxOpenConns" : 20 , "maxIdleConns" : 5 , "connMaxLifetime" : 300 } }
生产环境不建议把真实密码提交到仓库。可以提交 config.example.json,真实配置通过部署系统注入。
使用环境变量覆盖 环境变量适合覆盖少量环境相关配置,尤其是敏感信息:
1 2 3 4 5 6 7 8 9 10 11 func applyEnv (cfg *Config) { if v := os.Getenv("APP_ENV" ); v != "" { cfg.App.Env = v } if v := os.Getenv("HTTP_ADDR" ); v != "" { cfg.HTTP.Addr = v } if v := os.Getenv("DATABASE_DSN" ); v != "" { cfg.Database.DSN = v } }
启动时可以这样传入:
1 APP_ENV=prod DATABASE_DSN='user:pass@tcp(mysql:3306)/demo?parseTime=true' ./app
推荐优先级为:默认值 < 配置文件 < 环境变量。这样本地、测试和生产环境都能使用同一套加载逻辑。
启动时校验配置 配置错误应该在程序启动时暴露,而不是等到请求进来才报错:
1 2 3 4 5 6 7 8 9 10 11 12 func validate (cfg Config) error { if cfg.HTTP.Addr == "" { return errors.New("http addr is required" ) } if cfg.Database.DSN == "" { return errors.New("database dsn is required" ) } if cfg.Database.MaxOpenConns <= 0 { return errors.New("database maxOpenConns must be positive" ) } return nil }
校验逻辑不必复杂,但要覆盖必填项、端口格式、数值范围和枚举值。
在 main 中使用 入口函数只负责加载配置、初始化依赖和启动服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func main () { configPath := flag.String("config" , "" , "config file path" ) flag.Parse() cfg, err := LoadConfig(*configPath) if err != nil { log.Fatalf("load config failed: %v" , err) } db, err := openDB(cfg.Database) if err != nil { log.Fatalf("open database failed: %v" , err) } defer db.Close() server := &http.Server{ Addr: cfg.HTTP.Addr, ReadTimeout: time.Duration(cfg.HTTP.ReadTimeout) * time.Second, WriteTimeout: time.Duration(cfg.HTTP.WriteTimeout) * time.Second, Handler: newRouter(db), } log.Fatal(server.ListenAndServe()) }
不要在业务函数内部到处读取环境变量。配置应在启动时加载完成,然后作为结构体传给需要的模块。
常见问题 本地开发经常忘记配置数据库地址,可以提供 config.example.json 和 README 启动命令,但不要提交真实密码。
容器部署时,配置文件路径可能变化。建议程序支持 -config 参数,同时关键敏感项支持环境变量覆盖。
如果配置项很多,可以按模块拆分结构体,但不要把配置读取逻辑分散到多个包里,否则排查优先级和默认值会很困难。
小结 Go 项目配置管理不需要一开始就引入复杂配置中心。先用结构体明确配置形状,用默认值降低本地启动成本,用配置文件管理环境差异,用环境变量覆盖敏感信息,再在启动阶段做校验。这个方案简单、透明,也方便以后迁移到 Consul、Nacos 或 Kubernetes ConfigMap。