package csv import ( "context" "encoding/csv" "io" "strings" ) type RowHandler func(row []string, rowNumber int) error func ReadCSV(file io.Reader, handler RowHandler) (int, []string, error) { return ReadCSVWithContext(context.Background(), file, handler) } func ReadCSVWithContext(ctx context.Context, file io.Reader, handler RowHandler) (int, []string, error) { reader := csv.NewReader(file) reader.LazyQuotes = true reader.TrimLeadingSpace = true var errors []string rowNumber := 0 totalRows := 0 for { select { case <-ctx.Done(): return totalRows, errors, ctx.Err() default: } row, err := reader.Read() if err == io.EOF { break } rowNumber++ if err != nil { errors = append(errors, "Row "+itoa(rowNumber)+": "+err.Error()) continue } if len(row) == 0 || (len(row) == 1 && strings.TrimSpace(row[0]) == "") { continue } if rowNumber == 1 { continue } if err := handler(row, rowNumber); err != nil { if ctx.Err() != nil { return totalRows, errors, ctx.Err() } errors = append(errors, "Row "+itoa(rowNumber)+": "+err.Error()) } totalRows++ } return totalRows, errors, nil } func CountRows(file io.Reader) (int, error) { reader := csv.NewReader(file) count := 0 for { _, err := reader.Read() if err == io.EOF { break } if err != nil { continue } count++ } return count, nil } func WriteCSV(headers []string, rows [][]string) string { var sb strings.Builder writer := csv.NewWriter(&sb) writer.Write(headers) for _, row := range rows { writer.Write(row) } writer.Flush() return sb.String() } func itoa(i int) string { if i == 0 { return "0" } result := "" for i > 0 { result = string(rune('0'+i%10)) + result i /= 10 } return result }