テック技報最新号へ →
コード

Go: sync.OnceValue / OnceValues で「1回だけ初期化して値を返す」

Go 1.21 以降は sync.OnceValue(f) が「初回だけ f を実行し、以後は同じ値を返す関数」を作る。sync.Once とパッケージ変数を組み合わせる定型が 1 行になり、エラー付きは sync.OnceValues。

@amitsinha__07 · 投稿 2026-11-08
#go#concurrency#sync#lazy-init

使う場面

設定ファイルの読み込み、正規表現のコンパイル、重いクライアントの生成などを「最初に使われたときに 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本にまとめる(こちらは完了後に再実行される)。

出典

X の元の投稿を開く