Reputation: 1090
I've been pulling my hair out as to why this code throws the error:
package util
import (
"path/filepath"
"sync"
"github.com/go-ini/ini"
)
// ConfigMap is map for config values
type ConfigMap struct {
LogPath string
PublicDir string
SessionName string
Debug bool
DBUsersHost string
DBUsersName string
DBUsersUsername string
DBUsersPassword string
}
var once sync.Once
// Config loads and return config object
func Config() (*ConfigMap, error) {
once.Do(func() {
// Find the location of the app.conf file
configFilePath, err := filepath.Abs("../build/app.conf")
if err != nil {
return nil, err
}
// Load app.conf
cfg, err := ini.Load(configFilePath)
if err != nil {
return nil, err
}
// Get app mode
mode, err := AppMode()
if err != nil {
return nil, err
}
c := &ConfigMap{}
err = cfg.Section(mode).MapTo(c)
if err != nil {
return nil, err
}
return c, err
})
}
As you can see, the pairing is exactly correct. Each return code returns &ConfigMap
and err
and the function signature matches it. What am I missing?
Upvotes: 1
Views: 6630
Reputation: 417612
You pass an anonymous function value to once.Do()
(which is Once.Do()
), and the return
statements are inside that. Which means those return
statements want to return from the anonymous function, but it doesn't have any return values:
func Config() (*ConfigMap, error) {
once.Do(func() {
// You can't return any values here, only this works:
return
})
// And you do need to return something here:
return &ConfigMap{}, nil
}
What you may do is create global variables matching the return values of Config()
, and the anonymous function should store the values in them. And in Config()
you may return the values of these global variables.
var cm *ConfigMap
var cmErr error
func Config() (*ConfigMap, error) {
once.Do(func() {
// load config, and store, e.g.
cm, cmErr = &ConfigMap{}, nil
})
return cm, cmErr
}
Do we really need global variables? Since the values returned by Config()
are produced by the anonymous function passed to once.Do()
which is guaranteed to run only once, yes, you need to store them somewhere to be able to return them multiple times, even when the anonymous function is not called / run anymore (on subsequent calls to Config()
).
Question: May there be a data race here?
If Config()
is called from multiple goroutines, at least one will write the global variables cm
and cmErr
, and all will read them. So it's rightful to ask this question.
But the answer is no, the above code is safe. The global variables cm
and cmErr
are only written once, and this happens before they could be read. Because once.Do()
blocks until the anonymous function returns. If Config()
(and thus once.Do()
) is called simultaneously from multiple goroutines, all will block until the anonymous function completes (once only), and reading the variables can happen only after the first write. And since the anonymous function will not run anymore, no more writes will happen.
Upvotes: 9
Reputation: 58241
You're calling return nil, err
and similar from the nested func()
inside your once.Do
. Conversely, you're not returning from the actual function.
Instead, you can structure your code like this:
func newConfig() (*Config, error) {
configFilePath, err := filepath.Abs("../build/app.conf")
if err != nil {
return nil, err
}
// Load app.conf
cfg, err := ini.Load(configFilePath)
if err != nil {
return nil, err
}
// Get app mode
mode, err := AppMode()
if err != nil {
return nil, err
}
c := &ConfigMap{}
err = cfg.Section(mode).MapTo(c)
if err != nil {
return nil, err
}
return c, err
}
// Cached config and any error.
var (
cachedConfig *Config
cachedConfigErr error
)
func Config() (*Config, error) {
once.Do(func() {
cachedConfig, cachedConfigErr = newConfig()
})
return cachedConfig, cachedConfigErr
}
Upvotes: 1