From 972ef1cbeca43afc3bec12648edf88d138da0b17 Mon Sep 17 00:00:00 2001 From: LuSrackhall <142690689+LuSrackhall@users.noreply.github.com> Date: Wed, 23 Jul 2025 08:25:28 +0800 Subject: [PATCH] fix(watch): prevent memory leak by adding StopWatch method Add an explicit mechanism to stop the configuration watcher and release resources. Previously, the WatchConfig method started a goroutine that would only exit if the config file was removed or a watcher error occurred. This caused a memory leak when the Viper instance was no longer in use because the goroutine held a reference to it. Changes: - Added a `stopCh` channel to the Viper struct to signal the watcher goroutine to exit - Added a public method `StopWatch()` that closes the `stopCh` channel to stop the watcher - Modified the goroutine in `WatchConfig` to listen to `stopCh` and exit when closed - Added test case to verify goroutine termination and resource cleanup This allows the Viper instance to be garbage collected when there are no external references and StopWatch is called. Fixes #2038 --- viper.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/viper.go b/viper.go index e4bdda042..c64a26e8e 100644 --- a/viper.go +++ b/viper.go @@ -188,6 +188,8 @@ type Viper struct { experimentalFinder bool experimentalBindStruct bool + + stopCh chan struct{} } // New returns an initialized Viper instance. @@ -215,6 +217,8 @@ func New() *Viper { v.experimentalFinder = features.Finder v.experimentalBindStruct = features.BindStruct + + v.stopCh = make(chan struct{}) return v } @@ -340,11 +344,13 @@ func (v *Viper) WatchConfig() { eventsWG := sync.WaitGroup{} eventsWG.Add(1) go func() { + defer eventsWG.Done() for { select { + case <-v.stopCh: // 新增:监听停止信号 + return case event, ok := <-watcher.Events: if !ok { // 'Events' channel is closed - eventsWG.Done() return } currentConfigFile, _ := filepath.EvalSymlinks(filename) @@ -363,7 +369,6 @@ func (v *Viper) WatchConfig() { v.onConfigChange(event) } } else if filepath.Clean(event.Name) == configFile && event.Has(fsnotify.Remove) { - eventsWG.Done() return } @@ -371,7 +376,6 @@ func (v *Viper) WatchConfig() { if ok { // 'Errors' channel is not closed v.logger.Error(fmt.Sprintf("watcher error: %s", err)) } - eventsWG.Done() return } } @@ -383,6 +387,14 @@ func (v *Viper) WatchConfig() { initWG.Wait() // make sure that the go routine above fully ended before returning } +// StopWatch stops the configuration watcher and releases resources +func (v *Viper) StopWatch() { + if v.stopCh != nil { + close(v.stopCh) + v.stopCh = nil + } +} + // SetConfigFile explicitly defines the path, name and extension of the config file. // Viper will use this and not check any of the config paths. func SetConfigFile(in string) { v.SetConfigFile(in) }