Go

How do you manage configuration in Go (env vars, files, flags)? How do you validate it and reload it safely at run time?

Question 572MediumGo 1.22 to 1.25

Load everything into one typed struct at startup. Use a clear order of precedence: defaults, then file, then environment, then flags. Validate the whole struct and fail fast with an error listing every problem. Pass the config (or the parts each component needs) explicitly instead of reading os.Getenv deep in the code. This keeps the code testable and the configuration visible (12-factor apps prefer env vars).

For live reload, build a completely new config, validate it, then swap it in atomically. Readers always see either the old snapshot or the new one, never a partly updated one.

type Config struct {
	Addr      string        `json:"addr"`
	RateLimit int           `json:"rate_limit"`
	Timeout   time.Duration `json:"timeout"`
}

func (c Config) Validate() error {
	var errs []error
	if c.Addr == "" {
		errs = append(errs, errors.New("addr is required"))
	}
	if c.RateLimit <= 0 {
		errs = append(errs, fmt.Errorf("rate_limit must be > 0, got %d", c.RateLimit))
	}
	return errors.Join(errs...) // nil when errs is empty
}

var current atomic.Pointer[Config]

func load(path string) (*Config, error) {
	c := Config{Addr: ":8080", RateLimit: 100, Timeout: 5 * time.Second} // defaults
	if b, err := os.ReadFile(path); err == nil {
		if err := json.Unmarshal(b, &c); err != nil {
			return nil, fmt.Errorf("parse %s: %w", path, err)
		}
	}
	if v, ok := os.LookupEnv("APP_RATE_LIMIT"); ok {
		n, err := strconv.Atoi(v)
		if err != nil {
			return nil, fmt.Errorf("APP_RATE_LIMIT: %w", err)
		}
		c.RateLimit = n
	}
	return &c, c.Validate()
}

func watchSIGHUP(path string) {
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, syscall.SIGHUP)
	for range ch {
		c, err := load(path)
		if err != nil {
			log.Printf("reload rejected, keeping old config: %v", err)
			continue
		}
		current.Store(c)
	}
}

Gotchas:

  • Some settings cannot be hot-reloaded: listen address, pool sizes, TLS mode. Document which fields reload, and restart the process for the others.
  • A Kubernetes ConfigMap is updated with a symlink swap. Watch the directory, not the file.
  • Readers should call current.Load() once per request and use that snapshot throughout.
  • Never log secrets from the config. Give secret types a redacting String() method.

More on Observability, Debugging & Production Operations

All 14 Observability, Debugging & Production Operations questions