Go: многопоточная запись в файл CSV

Разработка /
Разработка: Go: многопоточная запись в файл CSV
Иногда при разработке приложений на 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»
1 комментарий
LyoshaKiselev
А еще можно через канал писать. Так будет красивее даже.
Пусть канал слушает и пишет в файл одна горутина, а писать в канал может кто угодно. И мьютекс не нужен.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.