Map CSV Rows into Typed Go Structs

Decode CSV rows into typed structs with header checks, per-line error context, and rejection of negative numeric fields.

GoData Integrity

Code

snippet
package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"strconv"
	"strings"
)

type Record struct {
	SKU      string
	Quantity int
	Price    int
}

var header = []string{"sku", "quantity", "price_cents"}

func parseRecords(input string) ([]Record, error) {
	reader := csv.NewReader(strings.NewReader(input))
	reader.TrimLeadingSpace = true

	first, err := reader.Read()
	if err != nil {
		return nil, fmt.Errorf("read header: %w", err)
	}
	if len(first) != len(header) {
		return nil, fmt.Errorf("header has %d columns, want %d", len(first), len(header))
	}
	for index, name := range header {
		if first[index] != name {
			return nil, fmt.Errorf("column %d: got %q want %q", index, first[index], name)
		}
	}

	var records []Record
	for line := 2; ; line++ {
		row, err := reader.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("line %d: %w", line, err)
		}
		quantity, err := strconv.Atoi(row[1])
		if err != nil || quantity < 0 {
			return nil, fmt.Errorf("line %d: invalid quantity %q", line, row[1])
		}
		price, err := strconv.Atoi(row[2])
		if err != nil || price < 0 {
			return nil, fmt.Errorf("line %d: invalid price %q", line, row[2])
		}
		records = append(records, Record{SKU: row[0], Quantity: quantity, Price: price})
	}
	return records, nil
}

func main() {
	input := "sku,quantity,price_cents\nA-1,5,1200\nB-2,3,800\n"
	records, err := parseRecords(input)
	if err != nil {
		panic(err)
	}
	for _, record := range records {
		fmt.Printf("%s qty=%d price=%d\n", record.SKU, record.Quantity, record.Price)
	}
	if len(records) != 2 || records[0].SKU != "A-1" || records[0].Price != 1200 {
		panic("unexpected records")
	}
	if _, err := parseRecords("sku,quantity,price_cents\nA-1,-2,100\n"); err == nil {
		panic("negative quantity should fail")
	}
}