← 中文资源

简体中文版本

将 CSV 行映射为 Go 结构体

校验表头并把 CSV 行解析成带类型的结构体,逐行提供错误上下文。

阅读英文原文

功能说明

这段代码把 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")
	}
}