Какие риски возникают при передаче слайса в несколько горутин и как обеспечить безопасность?

Ответ

Да, слайс можно передавать в разные горутины, но это требует особой осторожности, так как может привести к гонке данных (data race).

Слайс в Go — это не сам массив данных, а структура-дескриптор, содержащая:

  1. Указатель на базовый массив в памяти.
  2. Длину (len).
  3. Ёмкость (cap).

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

Пример с проблемой (гонка данных):

В этом коде есть две проблемы: гонка данных при записи в s[i] и некорректное использование переменной цикла i в замыкании.

func main() {
    s := []int{1, 2, 3, 4, 5}
    var wg sync.WaitGroup

    wg.Add(len(s))
    for i := range s {
        go func() {
            // ОШИБКА: Небезопасная запись в общий слайс.
            // К тому же, переменная `i` будет иметь последнее значение из цикла.
            s[i] = s[i] * 2 
            wg.Done()
        }()
    }

    wg.Wait()
    fmt.Println(s) // Результат непредсказуем
}

Способы решения

  1. Использование мьютексов (sync.Mutex) Это классический способ синхронизации доступа к общим данным. Каждая горутина блокирует мьютекс перед доступом к слайсу и разблокирует после.

    func main() {
        s := []int{1, 2, 3, 4, 5}
        var wg sync.WaitGroup
        var mu sync.Mutex
    
        wg.Add(len(s))
        for i := 0; i < len(s); i++ {
            go func(idx int) {
                defer wg.Done()
                mu.Lock()         // Блокируем доступ
                s[idx] *= 2       // Безопасно изменяем
                mu.Unlock()       // Разблокируем
            }(i) // Передаем копию индекса в горутину
        }
    
        wg.Wait()
        fmt.Println(s) // [2 4 6 8 10] (порядок может отличаться)
    }
  2. Передача данных по значению (копирование) Если горутине не нужно изменять исходный слайс, а только читать данные, безопаснее передать ей копию элемента.

    func main() {
        s := []int{1, 2, 3}
        var wg sync.WaitGroup
        for i := range s {
            wg.Add(1)
            // Передаем значение s[i], а не индекс
            go func(val int) {
                defer wg.Done()
                // Работаем с локальной копией `val`
                fmt.Println(val * 2)
            }(s[i])
        }
        wg.Wait()
    }
  3. Использование каналов Каналы — идиоматичный способ для безопасного обмена данными между горутинами в Go. Можно отправлять данные или результаты обработки через каналы, избегая прямого доступа к общей памяти.