Каждая выгрузка — отдельный отчёт
CSV содержит записи с полями. Для каждой строки проверяйте наличие категории и целочисленную неотрицательную сумму; повреждённый CSV возвращайте как ошибку. Независимые документы можно анализировать параллельно, собирая готовые сводки в одном месте.
package main
import (
"encoding/csv"
"fmt"
"io"
"log"
"strconv"
"strings"
"sync"
)
type summary struct { cents, invalid int }
func summarizeCSV(text string) (summary, error) {
var out summary
reader := csv.NewReader(strings.NewReader(text))
reader.FieldsPerRecord = -1
if _, err := reader.Read(); err != nil {
return out, fmt.Errorf("read CSV header: %w", err)
}
for {
record, err := reader.Read()
if err == io.EOF {
return out, nil
}
if err != nil {
return out, fmt.Errorf("read CSV record: %w", err)
}
if len(record) != 2 || strings.TrimSpace(record[0]) == "" {
out.invalid++
continue
}
cents, err := strconv.Atoi(record[1])
if err != nil || cents < 0 {
out.invalid++
continue
}
out.cents += cents
}
}
func main() {
documents := []string{
"category,cents\nfood,1200\ntravel,300\nfood,invalid",
"category,cents\nbooks,800",
"category,cents\nrefund,-100",
}
results := make(chan summary, len(documents))
var workers sync.WaitGroup
for _, document := range documents {
workers.Add(1)
go func(text string) {
defer workers.Done()
item, err := summarizeCSV(text)
if err != nil {
log.Printf("summarize CSV: %v", err)
return
}
results <- item
}(document)
}
workers.Wait()
close(results)
totalCents, rejected := 0, 0
for item := range results {
totalCents += item.cents
rejected += item.invalid
}
fmt.Println("расходы в центах:", totalCents, "отклонённых записей:", rejected)
}
Анализируйте строки внутри одного CSV последовательно, а разные выгрузки запускайте в отдельных горутинах. Возвращайте сумму и число отклонённых строк как локальный результат, не используя общий изменяемый счётчик.