← Назад к вопросам

Зачем нужен sync.Once?

2.3 Middle🔥 171 комментариев
#Конкурентность и горутины#Производительность и оптимизация

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

sync.Once в Go: гарантия однократного выполнения

sync.Once — это тип из стандартной библиотеки Go (sync), который обеспечивает выполнение определенного действия строго один раз, даже в многопоточной среде. Это один из ключевых инструментов для управления инициализацией ресурсов, создания синглтонов и выполнения однократных операций в конкурентных программах.

Основная проблема, которую он решает

В многопоточных приложениях часто требуется выполнить какую-то операцию только один раз, например:

  • Инициализация глобального конфигурационного объекта
  • Открытие файла или подключение к базе данных
  • Регистрация обработчиков или плагинов
  • Загрузка статических данных

Если несколько горутин попытаются выполнить эту операцию одновременно без контроля, это может привести к:

  • Конкурентному выполнению самой операции несколько раз
  • Гонкам данных при инициализации ресурса
  • Непредсказуемому поведению программы
  • Утечке ресурсов (например, двойное открытие соединения)

Как работает sync.Once

sync.Once имеет предельно простой API — всего один метод:

func (o *Once) Do(f func())

Внутренняя реализация использует механизмы синхронизации (mutex и атомарные операции) для гарантии, что:

  1. Функция f выполнится не более одного раза
  2. Все горутины, вызвавшие Do, будут блокироваться до завершения первого вызова f
  3. Последующие вызовы Do немедленно возвращаются без выполнения f

Пример базового использования:

package main

import (
    "fmt"
    "sync"
)

var (
    once     sync.Once
    instance *Config
)

type Config struct {
    APIKey string
}

func loadConfig() *Config {
    // Предположим, это тяжелая операция: чтение файла, запрос к API
    fmt.Println("Загрузка конфигурации...")
    return &Config{APIKey: "secret-key"}
}

func GetConfig() *Config {
    once.Do(func() {
        instance = loadConfig()
    })
    return instance
}

func main() {
    // Многопоточный доступ к конфигурации
    for i := 0; i < 5; i++ {
        go func() {
            config := GetConfig()
            fmt.Printf("Получен конфиг: %v\n", config.APIKey)
        }()
    }
    // Ожидание завершения горутин
    time.Sleep(time.Second)
}

Вывод программы будет содержать только одно сообщение "Загрузка конфигурации...", несмотря на 5 конкурентных вызовов.

Ключевые особенности и преимущества

  1. Эффективность: После первого выполнения, последующие вызовы Do работают практически без накладных расходов — проверка осуществляется через атомарные операции.
  2. Безопасность для горутин: Гарантирует, что все горутины, вызвавшие Do, будут видеть результат выполнения f.
  3. Идиоматичность: Стал стандартным способом реализации синглтона в Go.

Распространенные сценарии использования

1. Инициализация глобальных ресурсов

var (
    dbOnce   sync.Once
    dbConn   *sql.DB
)

func GetDBConnection() *sql.DB {
    dbOnce.Do(func() {
        conn, err := sql.Open("postgres", "user=...")
        if err != nil {
            log.Fatal(err)
        }
        dbConn = conn
    })
    return dbConn
}

2. Ленивая инициализация (lazy initialization)

type Service struct {
    cacheOnce sync.Once
    cache     map[string]interface{}
}

func (s *Service) GetCache() map[string]interface{} {
    s.cacheOnce.Do(func() {
        s.cache = make(map[string]interface{})
        // Заполнение начальными данными
    })
    return s.cache
}

3. Однократное выполнение настройки

func initializeLogger() {
    log.SetFlags(log.LstdFlags | log.Lmicroseconds)
    log.Println("Логгер инициализирован")
}

func main() {
    var initOnce sync.Once
    
    // В разных местах программы можем безопасно вызывать инициализацию
    initOnce.Do(initializeLogger)
    
    // Даже если вызвать повторно — ничего не произойдет
    initOnce.Do(initializeLogger)
}

Ограничения и важные замечания

  1. Невозможно "перезагрузить": sync.Once выполняет функцию строго один раз на протяжении всего жизненного цикла программы. Если нужно выполнить операцию повторно, необходимо создать новый sync.Once.
  2. Паника обрабатывается особо: Если функция f вызывает панику, sync.Once считает выполнение "завершенным". Последующие вызовы Do не будут пытаться выполнить f снова.
  3. Не для повторяющихся задач: Не используйте sync.Once для периодических задач или задач, требующих повторного выполнения.

Альтернативы и сравнение

  • Инициализация при старте: Можно выполнять всё в init() функциях, но это не всегда возможно (ленивая инициализация).
  • Мьютексы с флагом: Реализация через sync.Mutex и флаг выполнения более сложна и подвержена ошибкам:
var (
    mu      sync.Mutex
    done    bool
    resource interface{}
)

func GetResource() interface{} {
    mu.Lock()
    if !done {
        resource = createResource()
        done = true
    }
    mu.Unlock()
    return resource
}

sync.Once делает эту логику проще, безопаснее и более эффективной.

Заключение

sync.Once — это важный инструмент в арсенале Go разработчика для работы с многопоточностью. Он предоставляет:

  • Простой и чистый API для однократного выполнения
  • Гарантированную безопасность в конкурентных сценариях
  • Эффективность благодаря оптимизированной реализации

Используйте sync.Once когда вам нужно гарантировать, что определенная операция выполнится только один раз, независимо от количества горутин, которые пытаются её выполнить. Это особенно важно для инициализации ресурсов, создания синглтонов и других сценариев, где многократное выполнение может привести к проблемам или неэффективному использованию ресурсов.