Go 中 encoding/csv 读写 CSV 文件

FreeGuideOnline 最新 2026-07-07

Go 中 encoding/csv 读写 CSV 文件

为什么需要 encoding/csv

encoding/csv 是 Go 标准库中的包,专门用于读写 CSV(逗号分隔值)格式的数据。它遵循 RFC 4180 标准,并提供了灵活的配置,可以处理各种方言的 CSV 文件(如自定义分隔符、引号转义等)。相比于手动解析字符串,使用该包可以避免大量边界条件问题,例如字段内嵌逗号、换行符、引号转义等。

读取 CSV 文件

基本读取:使用 csv.NewReader

csv.NewReader 接受一个 io.Reader,返回一个 *csv.Reader。通过 Read 方法可以逐行读取记录,每次返回 []string

package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"log"
	"strings"
)

func main() {
	data := "name,age,city\nAlice,30,New York\nBob,25,London"
	r := csv.NewReader(strings.NewReader(data))

	for {
		record, err := r.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(record) // [name age city] 等
	}
}

一次性读取所有记录:ReadAll

当 CSV 数据较小时,可以使用 ReadAll 将所有行读入内存。

r := csv.NewReader(strings.NewReader(data))
records, err := r.ReadAll()
if err != nil {
    log.Fatal(err)
}
for _, record := range records {
    fmt.Println(record)
}

配置 csv.Reader

csv.Reader 提供了多个导出字段用于自定义解析行为:

  • Comma:字段分隔符,默认为 ','
  • Comment:注释字符,如果设置,以该字符开头的行将被忽略。
  • FieldsPerRecord:每条记录的字段数。0 表示使用第一行的字段数作为预期值;负数表示不检查。
  • LazyQuotes:允许引号内的引号可以不转义(非标准但常见)。
  • TrimLeadingSpace:忽略字段前的空白字符。

示例:读取以制表符分隔的文件,并忽略空行开头的空格。

r := csv.NewReader(file)
r.Comma = '\t'
r.TrimLeadingSpace = true
r.Comment = '#'

写入 CSV 文件

基本写入:使用 csv.NewWriter

csv.NewWriter 接受一个 io.Writer,通过 Write 方法写入一条记录(字符串切片)。

package main

import (
	"encoding/csv"
	"os"
)

func main() {
	file, _ := os.Create("output.csv")
	defer file.Close()

	w := csv.NewWriter(file)
	defer w.Flush() // 确保所有缓存数据写入

	w.Write([]string{"name", "age", "city"})
	w.Write([]string{"Alice", "30", "New York"})
	w.Write([]string{"Bob", "25", "London"})

	// 检查写入过程中的错误
	if err := w.Error(); err != nil {
		panic(err)
	}
}

重要:调用 Flush 后应检查 Error(),因为写入错误可能是延迟的。

一次性写入所有记录:WriteAll

records := [][]string{
    {"name", "age", "city"},
    {"Alice", "30", "New York"},
    {"Bob", "25", "London"},
}
w := csv.NewWriter(os.Stdout)
w.WriteAll(records) // 内部会调用 Flush
if err := w.Error(); err != nil {
    log.Fatal(err)
}

配置 csv.Writer

  • Comma:分隔符,默认 ','
  • UseCRLF:是否使用 \r\n 作为行终止符(默认只用 \n)。

例如,输出 Excel 兼容的 CSV(分号分隔,CRLF 换行):

w.Comma = ';'
w.UseCRLF = true

处理特殊字符与引号

CSV 标准规定字段内如果包含分隔符、双引号或换行符,必须用双引号包裹,且里面的双引号需要转义(写成两个双引号)。encoding/csv 会自动处理这些规则:

  • 写入时Write 会根据内容决定是否添加引号,并对双引号进行转义。
  • 读取时Read 会自动去除外层引号并将转义的双引号还原为一个单引号。

自定义引号行为可以通过 LazyQuotes 处理非法格式的 CSV,但建议仅在处理非标准输入时使用。

错误处理最佳实践

  1. 逐行读取时:检查 err == io.EOF 来结束循环,其他错误应停止处理。
  2. 写入后:务必调用 Flush() 并检查 w.Error()
  3. 字段数验证:设置 FieldsPerRecord 为正数来强制校验每行字段数,防止数据错位。
  4. 处理 BOM:如果 CSV 文件可能带有 UTF‑8 BOM,需要在 io.Reader 上包装一个去除 BOM 的 reader,或者使用 golang.org/x/text/encoding 等包。
func readCSVWithBOM(filename string) ([][]string, error) {
    f, _ := os.Open(filename)
    defer f.Close()
    // 简单去除 BOM 的方式(仅适用于 UTF‑8)
    br := bufio.NewReader(f)
    rune, _, _ := br.ReadRune()
    if rune != '\uFEFF' {
        br.UnreadRune() // 不是 BOM,放回
    }
    r := csv.NewReader(br)
    return r.ReadAll()
}

参数与容错示例

忽略空行

Reader 默认会跳过完全不包含字符的空行(所有字段都空的行)。如果需要保留空行(例如空行代表缺失记录),需降低处理层级或自行实现分割。

指定每行字段数

r := csv.NewReader(file)
r.FieldsPerRecord = 5 // 要求严格5个字段

如果行字段数不符,Read 将返回错误 ErrFieldCount

允许字段前空格

r.TrimLeadingSpace = true

这一选项会去掉字段前的空格,对人为对齐的 CSV 很有用。

性能提示

  • 对于大文件,使用逐行 Read 而不是 ReadAll 以降低内存占用。
  • 写入时,可以适当调整缓冲区大小(csv.Writer 底层使用 bufio.Writer,可通过 Set Writer 方式自定义缓冲大小)。
  • 如果需要并发读取,建议将文件分段(注意行对齐)分别构造 Reader

完整示例:过滤并转换 CSV 数据

下面的程序读取一个 CSV,只保留年龄大于 20 的记录,并写入新文件,使用分号分隔且使用 CRLF 换行。

package main

import (
	"encoding/csv"
	"io"
	"log"
	"os"
	"strconv"
)

func main() {
	input, _ := os.Open("input.csv")
	defer input.Close()
	output, _ := os.Create("output.csv")
	defer output.Close()

	reader := csv.NewReader(input)
	reader.TrimLeadingSpace = true

	writer := csv.NewWriter(output)
	writer.Comma = ';'
	writer.UseCRLF = true
	defer func() {
		writer.Flush()
		if err := writer.Error(); err != nil {
			log.Fatal(err)
		}
	}()

	// 读取并写入标题
	header, err := reader.Read()
	if err != nil {
		log.Fatal(err)
	}
	writer.Write(header)

	for {
		record, err := reader.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatal(err)
		}
		// 假设年龄在第二列
		age, err := strconv.Atoi(record[1])
		if err != nil {
			continue // 跳过无法解析年龄的行
		}
		if age > 20 {
			writer.Write(record)
		}
	}
}

常见问题排查

  1. 读取到空字段或字段拆分错误:检查分隔符设置是否正确(如 TSV 应设置 Comma = '\t')。
  2. 写入后文件末尾缺少换行csv.Writer 会在每条记录后写入换行符,文件末尾可能没有最后的新的一行,这是正常的。
  3. 中文乱码:确保文件编码为 UTF‑8,或者使用 golang.org/x/text 转换编码。
  4. 引号处理异常:如果输入 CSV 中引号不成对,可能是数据错误,尝试设置 LazyQuotes = true 容忍。

小结

encoding/csv 包提供了简洁、标准兼容且可配置的 CSV 读写能力,是处理 CSV 数据的首选方案。掌握它的读写方法、配置项和错误处理模式,可以安全且高效地完成绝大多数 CSV 相关任务。