Что такое `sync.Once` и для решения каких задач он используется?

Ответ

sync.Once — это тип в стандартной библиотеке Go, который предоставляет механизм для выполнения определенного действия ровно один раз.

Основная задача sync.Once — потокобезопасная ленивая инициализация. Он гарантирует, что даже при одновременном вызове из множества горутин, функция, переданная в метод Do, будет выполнена только единожды.

Классические примеры использования:

  • Инициализация синглтона (Singleton): Например, создание единственного экземпляра подключения к базе данных или загрузка конфигурации.
  • Инициализация глобальных переменных: Когда требуется выполнить сложную или ресурсоемкую инициализацию при первом обращении.

Пример: Ленивая загрузка конфигурации

package main

import (
    "fmt"
    "sync"
)

var (
    once   sync.Once
    config map[string]string
)

func loadConfig() {
    fmt.Println("Загрузка конфигурации...")
    // Эмуляция долгой операции
    // time.Sleep(1 * time.Second)
    config = map[string]string{
        "api_key": "super-secret-key",
    }
}

// GetConfig возвращает конфигурацию, инициализируя её при первом вызове.
func GetConfig() map[string]string {
    once.Do(loadConfig) // `loadConfig` будет вызвана только один раз.
    return config
}

func main() {
    // Множество горутин могут одновременно вызывать GetConfig
    // но `loadConfig` выполнится только один раз.
    for i := 0; i < 10; i++ {
        go GetConfig()
    }
    fmt.Scanln()
    fmt.Println("Конфигурация получена:", GetConfig()["api_key"])
}

Ключевые особенности:

  • Потокобезопасность: Внутренняя реализация использует мьютекс и атомарные операции для гарантии однократного выполнения.
  • Надежность: Если функция, переданная в Do, паникует, sync.Once считает, что вызов состоялся. Последующие вызовы Do не будут выполнять эту функцию снова.
  • Идиоматичность: Это предпочтительный способ для однократной инициализации в Go, так как он проще и надежнее ручной реализации с помощью мьютекса и флага.