Go CSV 处理入门:读取表格数据、清洗字段和导出报表

本文讲解 Go encoding/csv 的读取、写入、字段清洗、表头映射和错误处理,帮助初学者处理简单数据导入导出。

CSV 是最朴素也最常见的数据交换格式

很多系统导入导出数据时,最后都会落到 CSV:用户列表、订单明细、库存表、运营报表、日志统计结果。CSV 看起来只是逗号分隔文本,但真正处理时会遇到引号、换行、空字段、表头、编码和字段数量不一致等问题。不要自己用 strings.Split(line, ",") 解析 CSV,标准库已经提供了 encoding/csv

Go 的 csv.Readercsv.Writer 都基于 io.Readerio.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 很适合做这类数据工具。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页