Files
bible/rules/patterns/import-export/contract.md
2026-03-01 17:16:50 +03:00

4.2 KiB

Contract: Import / Export Workflows

Version: 1.0

Import Workflow

Recommended stages:

  1. Upload
  2. Preview / Validate
  3. Confirm
  4. Execute
  5. Result 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-8
    • Content-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 — not 1234.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 %v or strconv.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/csv with csv.Writer and set csv.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 JOIN in 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.Writer buffers 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.