Go 配置校验入门:让服务启动时就发现问题

本文讲解 Go 服务配置读取和校验的基本做法,包括环境变量、默认值、必填项、端口、URL 和安全开关检查。

配置错误应该在启动时暴露

很多线上问题不是代码逻辑错,而是配置错:端口写错,数据库地址为空,第三方 API URL 少了协议,生产环境打开了调试开关,超时时间是 0。最糟糕的情况是服务启动成功,直到请求进来才暴露配置问题。

Go 小服务可以用很朴素的方式处理配置:集中读取环境变量,设置合理默认值,对必填项和危险组合做校验。不要在业务函数里到处 os.Getenv,也不要让配置错误藏到运行中。

这篇文章写一个简单配置加载器。

定义配置结构体

type Config struct {
	Env        string
	Addr       string
	DatabaseURL string
	APIBaseURL  string
	Debug      bool
	Timeout    time.Duration
}

读取环境变量:

func LoadConfig() (Config, error) {
	timeoutSeconds, err := getenvInt("TIMEOUT_SECONDS", 5)
	if err != nil {
		return Config{}, err
	}

	cfg := Config{
		Env:        getenv("APP_ENV", "development"),
		Addr:       ":" + getenv("PORT", "8080"),
		DatabaseURL: strings.TrimSpace(os.Getenv("DATABASE_URL")),
		APIBaseURL:  strings.TrimSpace(os.Getenv("API_BASE_URL")),
		Debug:      getenv("DEBUG", "false") == "true",
		Timeout:    time.Duration(timeoutSeconds) * time.Second,
	}

	if err := cfg.Validate(); err != nil {
		return Config{}, err
	}
	return cfg, nil
}

辅助函数:

func getenv(key, fallback string) string {
	value := strings.TrimSpace(os.Getenv(key))
	if value == "" {
		return fallback
	}
	return value
}

func getenvInt(key string, fallback int) (int, error) {
	value := strings.TrimSpace(os.Getenv(key))
	if value == "" {
		return fallback, nil
	}
	n, err := strconv.Atoi(value)
	if err != nil {
		return 0, fmt.Errorf("%s must be integer", key)
	}
	return n, nil
}

校验必填项和格式

func (c Config) Validate() error {
	var errs []error

	if c.Env == "production" && c.Debug {
		errs = append(errs, fmt.Errorf("DEBUG must be false in production"))
	}
	if c.DatabaseURL == "" {
		errs = append(errs, fmt.Errorf("DATABASE_URL is required"))
	}
	if c.APIBaseURL == "" {
		errs = append(errs, fmt.Errorf("API_BASE_URL is required"))
	} else if _, err := url.ParseRequestURI(c.APIBaseURL); err != nil {
		errs = append(errs, fmt.Errorf("API_BASE_URL is invalid: %w", err))
	}
	if c.Timeout <= 0 {
		errs = append(errs, fmt.Errorf("timeout must be positive"))
	}

	return errors.Join(errs...)
}

一次返回多个配置错误,比修一个重启一次更友好。这里使用 errors.Join,启动日志会显示所有问题。

启动时打印摘要

func LogConfig(logger *slog.Logger, cfg Config) {
	logger.Info("config loaded",
		"env", cfg.Env,
		"addr", cfg.Addr,
		"api_base_url", cfg.APIBaseURL,
		"debug", cfg.Debug,
		"timeout", cfg.Timeout.String(),
		"database_url_set", cfg.DatabaseURL != "",
	)
}

不要打印完整数据库连接串,因为里面可能有用户名和密码。可以打印是否设置,或者脱敏后的主机名。

main

func main() {
	logger := slog.Default()

	cfg, err := LoadConfig()
	if err != nil {
		logger.Error("load config failed", "err", err)
		os.Exit(1)
	}
	LogConfig(logger, cfg)

	// start server
}

配置错误时直接启动失败,比分散到请求过程中失败更好。

给配置写测试

配置加载函数很适合测试。Go 的 testing.T 提供了 Setenv

func TestLoadConfigMissingDatabaseURL(t *testing.T) {
	t.Setenv("DATABASE_URL", "")
	t.Setenv("API_BASE_URL", "https://api.example.com")

	_, err := LoadConfig()
	if err == nil {
		t.Fatal("expected error")
	}
	if !strings.Contains(err.Error(), "DATABASE_URL") {
		t.Fatalf("error = %v", err)
	}
}

测试合法配置:

func TestLoadConfigOK(t *testing.T) {
	t.Setenv("DATABASE_URL", "postgres://user:pass@localhost/db")
	t.Setenv("API_BASE_URL", "https://api.example.com")
	t.Setenv("TIMEOUT_SECONDS", "10")

	cfg, err := LoadConfig()
	if err != nil {
		t.Fatalf("LoadConfig() error = %v", err)
	}
	if cfg.Timeout != 10*time.Second {
		t.Fatalf("timeout = %s", cfg.Timeout)
	}
}

配置测试能防止以后改环境变量名、默认值或校验规则时不小心破坏启动流程。很多服务事故都来自配置,给配置加测试非常划算。

小结

Go 服务配置可以很简单:集中读取,设置默认值,转换类型,统一校验,启动时打印摘要。必填项、URL、端口、超时、安全开关都应该在启动时检查。

配置是运行环境和代码之间的边界。边界越清楚,部署越稳定。不要让业务函数到处读环境变量,也不要让明显配置错误拖到线上请求才暴露。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页