4.2 KiB
4.2 KiB
Contract: Import / Export Workflows
Version: 1.0
Import Workflow
Recommended stages:
UploadPreview / ValidateConfirmExecuteResult summary
Rules:
- Validation preview must be human-readable (table/list), not raw JSON only.
- Warnings and errors should be shown per row and in aggregate summary.
- Confirm step should clearly communicate scope and side effects.
Export Workflow
- User must explicitly choose export scope (
selected,filtered,all) when ambiguity exists. - Export format should be explicit (
csv,json, etc.). - Download response must set:
Content-Type: text/csv; charset=utf-8Content-Disposition: attachment; filename="..."
CSV Format Rules (Excel-compatible)
These rules are mandatory whenever CSV is exported for spreadsheet users.
Encoding and BOM
- Write UTF-8 BOM (
\xEF\xBB\xBF) as the very first bytes of the response. - Without BOM, Excel on Windows opens UTF-8 CSV as ANSI and garbles Cyrillic/special characters.
w.Write([]byte{0xEF, 0xBB, 0xBF})
Delimiter
- Use semicolon (
;) as the field delimiter, not comma. - Excel in Russian/European locale uses semicolon as the list separator.
- Comma-delimited files open as a single column in these locales.
Numbers
- Write decimal numbers with a comma as the decimal separator:
1 234,56— not1234.56. - Excel in Russian locale does not recognize period as a decimal separator in numeric cells.
- Format integers and floats explicitly; do not rely on Go's default
%vorstrconv.FormatFloat. - Use a thin non-breaking space (
\u202F) or regular space as a thousands separator when the value benefits from readability (e.g. prices, quantities > 9999).
// correct
fmt.Sprintf("%.2f", price) // then replace "." -> ","
strings.ReplaceAll(fmt.Sprintf("%.2f", price), ".", ",")
// wrong — produces "1234.56", Excel treats it as text in RU locale
fmt.Sprintf("%.2f", price)
Dates
- Write dates as
DD.MM.YYYY— the format Excel in Russian locale parses as a date cell automatically. - Do not use ISO 8601 (
2006-01-02) for user-facing CSV; it is not auto-recognized as a date in RU locale.
Text quoting
- Wrap any field that contains the delimiter (
;), a newline, or a double-quote in double quotes. - Escape embedded double-quotes by doubling them:
"". - Use
encoding/csvwithcsv.Writerand setcsv.Writer.Comma = ';'; it handles quoting automatically.
Streaming Export Architecture (Go)
For exports with potentially large row counts use a 3-layer streaming pattern. Never load all rows into memory before writing — stream directly to the response writer.
Handler → sets HTTP headers + writes BOM → calls Service
Service → delegates to Repository with a row callback
Repository → queries in batches → calls callback per row
Handler/Service → csv.Writer.Flush() after all rows
// Handler
func ExportCSV(c *gin.Context) {
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", `attachment; filename="export.csv"`)
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}) // BOM
w := csv.NewWriter(c.Writer)
w.Comma = ';'
w.Write([]string{"ID", "Name", "Price"}) // header row
err := svc.StreamRows(ctx, filters, func(row Row) error {
return w.Write([]string{row.ID, row.Name, formatPrice(row.Price)})
})
w.Flush()
if err != nil {
// headers already sent — log only, cannot change status
slog.Error("csv export failed mid-stream", "err", err)
}
}
// Repository — batch fetch with callback
func (r *Repo) StreamRows(ctx, filters, fn func(Row) error) error {
rows, err := r.db.QueryContext(ctx, query, args...)
// ... scan and call fn(row) for each row
}
- Use
JOINin the repository query to avoid N+1 per row. - Batch size is optional; streaming row-by-row is fine for most datasets.
- Always call
w.Flush()after the loop —csv.Writerbuffers internally.
Error Handling
- Import errors should map to clear user-facing messages.
- Export errors after streaming starts must be logged server-side only — HTTP headers are already sent and the status code cannot be changed mid-stream.