配置管理是后端项目里很基础但容易混乱的部分。服务端程序通常需要数据库地址、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。