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.
Code
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")
}
}
Report a problem with this exampleOpens a pre-filled issue on GitHub.