CtrlK
BlogDocsLog inGet started
Tessl Logo

tpscloud/gophers

Source-verified agent skills for Go: Cobra command-line interfaces, Viper configuration, and Wails desktop apps, each backed by tested examples.

99

1.02x
Quality

100%

Does it follow best practices?

Impact

99%

1.02x

Average score across 18 eval scenarios

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

reload-and-write.mdskills/viper/references/

Reloads, writes, and provenance

Behavior below is for Viper v1.21.0; each statement has a test in examples/viper/behavior/.

What WatchConfig does

WatchConfig starts a goroutine that watches the config file's directory. When the file is written or created, it calls ReadInConfig on the same instance and then the OnConfigChange hook. Know these before relying on it:

  • The hook runs even when the reread failed (a malformed edit). The instance keeps its old values, so the hook cannot tell a good reload from a bad one by reading it.
  • Removing the file ends the watch for good. On macOS, replacing the file by rename, the way many editors and config tools save atomically, is reported as a removal and also ends the watch. Test reloads on each OS you ship.
  • WatchConfig returns nothing. If the watcher cannot be registered, for example because the file's directory does not exist, Viper logs the error and the hook never runs. Load the file once before watching so a bad path fails at startup.
  • *viper.Viper has no method to stop a watcher.
  • The reread writes into the instance without locking, on the watcher goroutine. Do not read that instance from other goroutines.

Skip watching in short-lived CLI commands.

A reload that keeps the last good snapshot

Watch with an instance nobody reads, and on each change run the normal loader on a fresh instance. A bad edit is reported; the published snapshot stays:

// examples/viper/config/watch.go
func Watch(src Source, apply func(Config), report func(error)) error {
	path, err := resolve(src)
	if err != nil {
		return err
	}
	if path == "" {
		return errors.New("no config file to watch")
	}
	src.File, src.Dirs = path, nil // reload the file being watched, never a newer find
	if _, err := Load(src); err != nil {
		return err // WatchConfig only logs a failed registration
	}
	w := viper.New()
	w.SetConfigFile(path)
	w.OnConfigChange(func(fsnotify.Event) {
		cfg, err := Load(src)
		if err != nil {
			report(err)
			return
		}
		apply(cfg)
	})
	w.WatchConfig()
	return nil
}

apply runs on the watcher goroutine. Publish with an atomic.Pointer[Config], a mutex, or a channel send. In a TUI, send the snapshot to the UI loop as a message rather than changing the model from the watcher goroutine. The test in examples/viper/config/config_test.go applies a valid edit, then checks that an invalid one is reported and the previous snapshot survives. Other tests there check that a file created later in a higher-priority directory does not take over the reload, and that a missing file fails Watch instead of returning nil.

When a program needs cancellation or a bounded lifetime, use its own fsnotify watcher and call Load on events instead of WatchConfig.

Writing config files

WriteConfig, WriteConfigAs, and WriteConfigTo serialize every effective setting, so defaults and env values (including secrets such as a token) land in the file next to the file's own values. Comments in the original file are lost. SafeWriteConfigAs only adds a check: it refuses an existing file with viper.ConfigFileAlreadyExistsError.

For a command that edits the user's config, read the user's file into its own document, change the intended keys, and write that back yourself. Writing a temp file and renaming it survives crashes, but a running WatchConfig on macOS stops at the rename. Do not rewrite files another tool owns, such as ones generated by Nix or a configuration manager.

Sections and provenance

  • Sub("server") returns nil when the key is missing or holds a scalar. Decode typed sections at the boundary instead of passing sub-instances around.
  • IsSet is true for a key with only a SetDefault; it does not mean the user set anything. InConfig looks only at the file. ConfigFileUsed returns the file that was read, including one found by discovery.
  • None of these say which layer won for a key. If the product must explain that (a config show --origin command, for example), record it in the loader as you bind each layer, and redact secrets in the output.

Remote key/value providers (github.com/spf13/viper/remote) are outside this skill. Add one only for a real remote-settings requirement, and check its dependencies, credentials, and retry behavior in the selected version.

README.md

tile.json