Иногда при разработке приложений на Go бывает необходимость записи в файл CSV из нескольких горутин, при этом встроенный CSV Writer непотокобезопасен.
Моя первая попытка записи в файл CSV выглядела так:
package main
import (
"encoding/csv"
"os"
"log"
"strconv"
)
func main() {
csvFile, err := os.Create("/tmp/foo.csv")
if err != nil {
log.Panic(err)
}
w := csv.NewWriter(csvFile)
w.Write([]string{"id1","id2","id3"})
count := 100
done := make(chan bool, count)
for i := 0; i < count; i++ {
go func(i int) {
w.Write([]string {strconv.Itoa(i), strconv.Itoa(i), strconv.Itoa(i)})
done <- true
}(i)
}
for i:=0; i < count; i++ {
<- done
}
w.Flush()
}
Этот сценарий должен выводить числа от 0 до 99 по три на строку. Некоторые строки записались правильно, но как мы видим, некоторые неправильно:
40,40,40
37,37,37
38,38,38
18,18,39
^@,39,39
...
67,67,70,^@70,70
65,65,65
73,73,73
66,66,66
72,72,72
75,74,75,74,75
74
7779^@,79,77
...
Есть способ, которым мы можем сделать наш код безопасным - использовать мьютекс при вызове методов CSV Writer-а. И я написал такой код:
type CsvWriter struct {
mutex *sync.Mutex
csvWriter *csv.Writer
}
func NewCsvWriter(fileName string) (*CsvWriter, error) {
csvFile, err := os.Create(fileName)
if err != nil {
return nil, err
}
w := csv.NewWriter(csvFile)
return &CsvWriter{csvWriter:w, mutex: &sync.Mutex{}}, nil
}
func (w *CsvWriter) Write(row []string) {
w.mutex.Lock()
w.csvWriter.Write(row)
w.mutex.Unlock()
}
func (w *CsvWriter) Flush() {
w.mutex.Lock()
w.csvWriter.Flush()
w.mutex.Unlock()
}
Мы создаём мьютекс когда NewCsvWriter создаёт экземпляры CsvWriter, затем используем его в функциях Write и Flush, таким образом только одна горутина одновременно имеет доступ к базовому CsvWriter-у. Немного изменим наш код так, чтобы не вызывать CsvWriter напрямую:
func main() {
w, err := NewCsvWriter("/tmp/foo-safe.csv")
if err != nil {
log.Panic(err)
}
w.Write([]string{"id1","id2","id3"})
count := 100
done := make(chan bool, count)
for i := 0; i < count; i++ {
go func(i int) {
w.Write([]string {strconv.Itoa(i), strconv.Itoa(i), strconv.Itoa(i)})
done <- true
}(i)
}
for i:=0; i < count; i++ {
<- done
}
w.Flush()
}
Сейчас если мы посмотрим в файл CSV, то увидим, что все строки корректны:
...
25,25,25
13,13,13
29,29,29
32,32,32
26,26,26
30,30,30
27,27,27
31,31,31
28,28,28
34,34,34
35,35,35
33,33,33
37,37,37
36,36,36
...
Источник: "Go: Multi-threaded writing to a CSV file"
Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.
Пишите: @ighar. Buy me a coffee, please :).