Go の Once で初期化処理を一度だけ実行する
アプリケーションの初期化処理は一度だけ実行したいものです。データベース接続の確立、設定ファイルの読み込み、シングルトンの生成など、複数回実行すると問題になる処理があります。sync.Once はそのような処理を goroutine-safe に一度だけ実行することを保証します。
sync.Once の基本
sync.Once は Do メソッドを持ち、渡された関数を最初の 1 回だけ実行します。
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
for i := 0; i < 5; i++ {
once.Do(func() {
fmt.Println("初期化処理")
})
fmt.Println("ループ:", i)
}
}出力は以下のようになります。「初期化処理」は 1 回しか表示されません。
初期化処理
ループ: 0
ループ: 1
ループ: 2
ループ: 3
ループ: 4複数の goroutine からの呼び出し
sync.Once の真価は複数の goroutine から同時に呼び出されても安全な点です。
func main() {
var once sync.Once
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
once.Do(func() {
fmt.Println("初期化実行者:", id)
})
fmt.Println("goroutine:", id)
}(i)
}
wg.Wait()
}10 個の goroutine が同時に once.Do を呼び出しても、初期化処理は 1 回だけ実行されます。どの goroutine が実行者になるかは不定ですが、他の goroutine は初期化が完了するまで待機します。
シングルトンパターン
sync.Once はシングルトンの実装によく使われます。
type Database struct {
connection string
}
var (
dbInstance *Database
dbOnce sync.Once
)
func GetDatabase() *Database {
dbOnce.Do(func() {
fmt.Println("データベース接続を確立")
dbInstance = &Database{
connection: "host=localhost port=5432",
}
})
return dbInstance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
db := GetDatabase()
fmt.Println("取得:", db.connection)
}()
}
wg.Wait()
}何度 GetDatabase() を呼んでも、データベース接続は 1 回だけ確立されます。
設定ファイルの遅延読み込み
起動時ではなく、必要になったときに設定を読み込む「遅延初期化」にも使えます。
type Config struct {
APIKey string
Endpoint string
}
var (
config *Config
configOnce sync.Once
)
func GetConfig() *Config {
configOnce.Do(func() {
fmt.Println("設定ファイルを読み込み中...")
// 実際にはファイルから読み込む
config = &Config{
APIKey: "secret-key",
Endpoint: "https://api.example.com",
}
})
return config
}設定が必要になるまで読み込みを遅延させることで、使われない設定の読み込みコストを削減できます。
注意点
sync.Once にはいくつかの注意点があります。
クロージャを使って外部の変数をキャプチャする必要があります。
Do 内でパニックが発生しても、2 回目以降は実行されません。初期化のエラーハンドリングには別の仕組みが必要です。
パニック時の挙動は要注意です。初期化に失敗した場合でも再試行されないため、エラー処理を慎重に設計する必要があります。
エラーを扱うパターン
初期化時のエラーを扱うには、結果を変数に保存するパターンが使えます。
var (
client *http.Client
clientErr error
clientOnce sync.Once
)
func GetClient() (*http.Client, error) {
clientOnce.Do(func() {
// 初期化処理(エラーが発生する可能性あり)
client = &http.Client{Timeout: 10 * time.Second}
// clientErr = errors.New("初期化失敗") // エラー時
})
return client, clientErr
}このパターンでは、初期化が成功したか失敗したかを呼び出し元で判断できます。ただし、失敗時の再試行はできません。再試行が必要な場合は、sync.Once 以外の仕組み(Mutex + フラグなど)を検討してください。