Go: sync.OnceValue / OnceValues で「1回だけ初期化して値を返す」
Go 1.21 以降は sync.OnceValue(f) が「初回だけ f を実行し、以後は同じ値を返す関数」を作る。sync.Once とパッケージ変数を組み合わせる定型が 1 行になり、エラー付きは sync.OnceValues。
使う場面
設定ファイルの読み込み、正規表現のコンパイル、重いクライアントの生成などを「最初に使われたときに 1 回だけ」行いたいとき。init() で起動時に必ず実行するのは避けたいが、並行に呼ばれても二重初期化はさせたくない、という場面。
やり方
go
// 以前の書き方
var (
cfgOnce sync.Once
cfg *Config
)
func getConfig() *Config {
cfgOnce.Do(func() { cfg = mustLoad() })
return cfg
}
// Go 1.21+
var getConfig = sync.OnceValue(func() *Config {
return mustLoad()
})
// エラーも返したいとき
var loadConfig = sync.OnceValues(func() (*Config, error) {
return load("config.yaml")
})
func handler() error {
cfg, err := loadConfig() // 何回呼んでも load は 1 回
if err != nil {
return err
}
_ = cfg
return nil
}- 値を返さない初期化だけなら
sync.OnceFunc(f)。 - 呼び出しは並行安全。初回実行中に来た呼び出しは完了まで待つ。
注意点
OnceValuesは エラーもキャッシュする。一時的な失敗(ネットワークなど)でもずっと同じエラーが返るので、リトライしたいものには向かない。- f がパニックすると、以後の呼び出しも同じパニックを起こす(
sync.Onceと違い「完了扱いで黙る」ことはない)。 - テストで値を差し替えたい場合は、パッケージ変数ではなく依存として渡す設計のほうが扱いやすい。
- 関連: 同じキーの同時呼び出しを 1 本にまとめるなら Go: singleflight で同じキーの同時呼び出しを1本にまとめる(こちらは完了後に再実行される)。