功能说明
这段代码把 CSV 文本解析成带类型的 Go 结构体切片。它先核对表头顺序和列数,再逐行把数量、价格转换成整数,并拒绝负数或非数字值。相比直接把每行当作字符串切片使用,结构体让后续业务代码有明确字段名和类型约束。
逐行错误信息会带上行号,便于定位脏数据。这个模式适合导入报表、库存表、账单或运营后台导出的 CSV 文件。
关键代码
parseRecords 用 csv.NewReader(strings.NewReader(input)) 创建读取器,TrimLeadingSpace 忽略字段前的空格。第一次 Read 专门读取表头,检查列数和每一列名称是否与 header 完全一致。
随后从第 2 行开始循环读取:io.EOF 表示正常结束;其他错误用 %w 包装并带上行号。数量和价格通过 strconv.Atoi 转换,同时检查负值;所有校验通过后才追加到 records。
使用注意
csv.Reader 默认要求每行列数一致,遇到不规则行会直接返回错误;如果业务允许缺列,需要设置 FieldsPerRecord = -1 并自行补默认值。表头必须与代码顺序完全一致,大小写或空格差异都会被拒绝。
UTF-8 BOM 会让第一列表头变成 sku,导入前应去掉 BOM。代码没有限制文件大小和行数,处理用户上传时应配合 io.LimitReader 和最大行数。金额使用整数分存储,避免浮点误差。
验证方式
示例输入两行数据,解析后第一行 SKU 为 A-1、数量 5、价格 1200,断言通过。把数量改成 -2 会返回带行号的错误,说明校验生效。可以再加入引号包裹的逗号字段和空行,确认 encoding/csv 的标准行为符合预期。
代码示例
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")
}
}