CSV 是最朴素也最常见的数据交换格式
很多系统导入导出数据时,最后都会落到 CSV:用户列表、订单明细、库存表、运营报表、日志统计结果。CSV 看起来只是逗号分隔文本,但真正处理时会遇到引号、换行、空字段、表头、编码和字段数量不一致等问题。不要自己用 strings.Split(line, ",") 解析 CSV,标准库已经提供了 encoding/csv。
Go 的 csv.Reader 和 csv.Writer 都基于 io.Reader、io.Writer,可以读文件、读 HTTP 上传内容,也可以写到内存或响应体。掌握它们后,你能很快写出数据导入和报表导出工具。
这篇文章用用户导入和状态报表导出做例子,讲解 CSV 常用写法。
读取 CSV 文件
CSV 内容:
email,name,age
xiaolin@example.com,小林,28
azhou@example.com,阿周,31
读取:
func ReadUsers(r io.Reader) ([]User, error) {
reader := csv.NewReader(r)
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("read csv: %w", err)
}
if len(records) == 0 {
return []User{}, nil
}
var users []User
for i, record := range records[1:] {
if len(record) != 3 {
return nil, fmt.Errorf("line %d: expected 3 fields", i+2)
}
age, err := strconv.Atoi(strings.TrimSpace(record[2]))
if err != nil {
return nil, fmt.Errorf("line %d: invalid age", i+2)
}
users = append(users, User{
Email: strings.TrimSpace(record[0]),
Name: strings.TrimSpace(record[1]),
Age: age,
})
}
return users, nil
}
结构体:
type User struct {
Email string
Name string
Age int
}
ReadAll 简单,但会把所有数据读入内存。小文件没问题,大文件更建议逐行读。
逐行读取更适合大文件
func ReadUsersStream(r io.Reader) ([]User, error) {
reader := csv.NewReader(r)
header, err := reader.Read()
if err != nil {
if err == io.EOF {
return []User{}, nil
}
return nil, err
}
if len(header) != 3 {
return nil, fmt.Errorf("invalid header")
}
var users []User
line := 1
for {
line++
record, err := reader.Read()
if err != nil {
if err == io.EOF {
break
}
return nil, fmt.Errorf("line %d: %w", line, err)
}
if len(record) != 3 {
return nil, fmt.Errorf("line %d: expected 3 fields", line)
}
age, err := strconv.Atoi(strings.TrimSpace(record[2]))
if err != nil {
return nil, fmt.Errorf("line %d: invalid age", line)
}
users = append(users, User{
Email: strings.TrimSpace(record[0]),
Name: strings.TrimSpace(record[1]),
Age: age,
})
}
return users, nil
}
逐行读取能更早发现错误,也不用一次占用大量内存。真实导入系统通常还会边读边批量写数据库,而不是先攒完整切片。
使用表头映射字段
如果 CSV 列顺序可能变化,可以把表头映射成下标:
func headerIndex(header []string) map[string]int {
index := make(map[string]int)
for i, name := range header {
index[strings.TrimSpace(name)] = i
}
return index
}
读取字段:
idx := headerIndex(header)
email := record[idx["email"]]
name := record[idx["name"]]
使用前要检查必填列是否存在:
for _, name := range []string{"email", "name", "age"} {
if _, ok := idx[name]; !ok {
return nil, fmt.Errorf("missing column %s", name)
}
}
这样导入工具对列顺序更宽容,但仍然要求关键列存在。
写出 CSV 报表
type StatusCount struct {
Status string
Count int
}
func WriteStatusReport(w io.Writer, counts []StatusCount) error {
writer := csv.NewWriter(w)
defer writer.Flush()
if err := writer.Write([]string{"status", "count"}); err != nil {
return err
}
for _, item := range counts {
record := []string{
item.Status,
strconv.Itoa(item.Count),
}
if err := writer.Write(record); err != nil {
return err
}
}
if err := writer.Error(); err != nil {
return fmt.Errorf("write csv: %w", err)
}
return nil
}
csv.Writer 会处理必要的引号和转义。写完后要 Flush,并检查 writer.Error()。
写到 HTTP 响应:
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="report.csv"`)
err := WriteStatusReport(w, counts)
这样浏览器会下载 CSV 文件。
小结
CSV 不要手写 strings.Split 解析,使用标准库 encoding/csv 更可靠。小文件可以 ReadAll,大文件逐行 Read;导入时要清洗字段、检查表头、给错误带行号;导出时使用 csv.Writer,写完 Flush 并检查错误。
CSV 常用于运营和后台系统,格式朴素但边界不少。把读取、校验和写出流程整理清楚,Go 很适合做这类数据工具。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。