配置错误应该在启动时暴露
很多线上问题不是代码逻辑错,而是配置错:端口写错,数据库地址为空,第三方 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、端口、超时、安全开关都应该在启动时检查。
配置是运行环境和代码之间的边界。边界越清楚,部署越稳定。不要让业务函数到处读环境变量,也不要让明显配置错误拖到线上请求才暴露。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。