encoding/csv - CSV 文件编解码
概述
encoding/csv 包提供了 CSV(Comma-Separated Values)文件的读写功能。
CSV 是什么:
- 📦 表格数据格式:用逗号分隔的纯文本表格格式
- 🔧 通用数据交换:广泛应用于数据导入导出
- 📋 行列结构:每行一条记录,每列一个字段
- 🛠️ 简单易用:人类可读,机器易解析
主要用途:
- 🌐 数据导出:数据库、Excel 导出数据
- 📧 数据导入:批量导入用户、产品等数据
- 🔐 配置文件:简单的配置数据存储
- 📊 数据分析:数据科学、统计分析
- 🖼️ 报表生成:生成电子表格兼容的报表
- 🔑 日志记录:结构化日志存储
重要说明:
- ⚠️ RFC 4180 标准:遵循 CSV 标准规范
- ⚠️ 字段分隔符:默认为逗号,可自定义
- ⚠️ 引用规则:包含特殊字符的字段需用引号包围
- ⚠️ 转义字符:引号内的引号用双引号转义
- ✅ 标准库支持:Go 标准库提供完整支持
- ✅ 流式处理:支持逐行读写大文件
- ✅ 自定义配置:可配置分隔符、引号等
CSV 示例:
name,age,city,email
John,30,New York,john@example.com
Jane,25,Los Angeles,jane@example.com
"Bob, Jr.",35,"San Francisco",bob@example.com
CSV 格式规范
RFC 4180 标准
基本规则:
- 每行一条记录,以 CRLF(\r\n)或 LF(\n)结尾
- 字段之间用逗号分隔
- 字段可以包含或不包含引号
- 如果字段包含以下字符,必须用引号包围:
- 逗号(,)
- 换行符(\n 或 \r\n)
- 双引号(“)
- 引号内的双引号用两个双引号表示(“”)
示例:
# 普通字段
John,30,New York
# 包含逗号的字段(需要引号)
"Doe, John",30,New York
# 包含引号的字段(需要转义)
John,"He said ""Hello""",30
# 包含换行的字段(需要引号)
John,30,"Line 1
Line 2"
特殊字符处理
| 字符 | 处理方式 | 示例 |
|---|---|---|
| 逗号 | 用引号包围 | "Smith, John" |
| 换行符 | 用引号包围 | "Line1\nLine2" |
| 双引号 | 双引号转义 | "He said ""Hi""" |
| 空格 | 保留原样 | John |
核心类型
1. Reader - CSV 读取器
type Reader struct {
// 字段分隔符(默认为 ',')
Comma rune
// 注释字符(默认为 0,表示禁用)
Comment rune
// 是否允许每行字段数不同(默认 false)
FieldsPerRecord int
// 是否去除字段前后的空格(默认 false)
TrimLeadingSpace bool
// 引号字符(默认为 '"')
Quote rune
// 是否禁用引号(默认 false)
DisableQuote bool
// 是否启用换行符检测(Go 1.20+)
ReuseRecord bool
}
功能:从输入流读取 CSV 数据。
创建方法:
func NewReader(r io.Reader) *Reader
主要方法:
// 读取一条记录(一行)
func (r *Reader) Read() ([]string, error)
// 读取所有记录
func (r *Reader) ReadAll() ([][]string, error)
配置选项:
// 设置字段分隔符
reader.Comma = ';' // 分号分隔
// 设置注释字符
reader.Comment = '#' // # 开头的行为注释
// 允许字段数可变
reader.FieldsPerRecord = -1
// 去除前导空格
reader.TrimLeadingSpace = true
// 禁用引号
reader.DisableQuote = true
2. Writer - CSV 写入器
type Writer struct {
// 字段分隔符(默认为 ',')
Comma rune
// 是否总是使用引号(默认 false)
UseCRLF bool
// 引号字符(默认为 '"')
Quote rune
// 是否禁用引号(默认 false)
DisableQuote bool
}
功能:将数据写入 CSV 格式。
创建方法:
func NewWriter(w io.Writer) *Writer
主要方法:
// 写入一条记录
func (w *Writer) Write(record []string) error
// 写入多条记录
func (w *Writer) WriteAll(records [][]string) error
// 刷新缓冲区
func (w *Writer) Flush() error
// 检查错误
func (w *Writer) Error() error
使用模式:
// 1. 单条写入
writer.Write([]string{"John", "30", "New York"})
writer.Flush()
// 2. 批量写入
writer.WriteAll([][]string{
{"John", "30", "New York"},
{"Jane", "25", "Los Angeles"},
})
// 3. 检查错误
if err := writer.Error(); err != nil {
log.Fatal(err)
}
核心函数
1. ParseCSVLine - 解析 CSV 行
func ReadAll(r io.Reader) ([][]string, error)
功能:便捷函数,直接读取所有 CSV 数据。
示例:
data := strings.NewReader("name,age\nJohn,30\nJane,25")
records, err := csv.NewReader(data).ReadAll()
完整示例
示例 1:基本读写
package main
import (
"encoding/csv"
"fmt"
"log"
"os"
"strings"
)
func main() {
fmt.Println("=== CSV 基本读写 ===\n")
// 1. 创建 CSV 数据
csvData := `name,age,city,email
John Doe,30,New York,john@example.com
Jane Smith,25,Los Angeles,jane@example.com
"Bob, Jr.",35,"San Francisco",bob@example.com`
// 2. 读取 CSV
fmt.Println("读取 CSV 数据:")
reader := csv.NewReader(strings.NewReader(csvData))
records, err := reader.ReadAll()
if err != nil {
log.Fatal(err)
}
// 3. 显示读取结果
for i, record := range records {
fmt.Printf("行 %d: %v\n", i, record)
}
// 4. 写入 CSV
fmt.Println("\n写入 CSV 数据:")
file, err := os.Create("output.csv")
if err != nil {
log.Fatal(err)
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// 写入数据
err = writer.WriteAll(records)
if err != nil {
log.Fatal(err)
}
fmt.Println("✓ CSV 文件已写入 output.csv")
// 5. 验证写入
fmt.Println("\n验证写入的文件:")
file2, err := os.Open("output.csv")
if err != nil {
log.Fatal(err)
}
defer file2.Close()
reader2 := csv.NewReader(file2)
records2, err := reader2.ReadAll()
if err != nil {
log.Fatal(err)
}
fmt.Printf("读取记录数:%d\n", len(records2))
for i, record := range records2 {
fmt.Printf("行 %d: %v\n", i, record)
}
// 清理
os.Remove("output.csv")
}
输出:
=== CSV 基本读写 ===
读取 CSV 数据:
行 0: [name age city email]
行 1: [John Doe 30 New York john@example.com]
行 2: [Jane Smith 25 Los Angeles jane@example.com]
行 3: [Bob, Jr. 35 San Francisco bob@example.com]
写入 CSV 数据:
✓ CSV 文件已写入 output.csv
验证写入的文件:
读取记录数:4
行 0: [name age city email]
行 1: [John Doe 30 New York john@example.com]
行 2: [Jane Smith 25 Los Angeles jane@example.com]
行 3: [Bob, Jr. 35 San Francisco bob@example.com]
示例 2:自定义分隔符
package main
import (
"encoding/csv"
"fmt"
"log"
"strings"
)
func main() {
fmt.Println("=== 自定义分隔符 ===\n")
// 1. 分号分隔的 CSV(欧洲格式)
semicolonCSV := `name;age;city
John;30;New York
Jane;25;Los Angeles`
fmt.Println("分号分隔:")
reader := csv.NewReader(strings.NewReader(semicolonCSV))
reader.Comma = ';' // 设置分隔符为分号
records, err := reader.ReadAll()
if err != nil {
log.Fatal(err)
}
for _, record := range records {
fmt.Printf(" %v\n", record)
}
// 2. 制表符分隔的 CSV(TSV 格式)
fmt.Println("\n制表符分隔 (TSV):")
tsvData := "name\tage\tcity\nJohn\t30\tNew York"
reader = csv.NewReader(strings.NewReader(tsvData))
reader.Comma = '\t' // 设置分隔符为制表符
records, err = reader.ReadAll()
if err != nil {
log.Fatal(err)
}
for _, record := range records {
fmt.Printf(" %v\n", record)
}
// 3. 写入自定义分隔符
fmt.Println("\n写入管道符分隔:")
var buf strings.Builder
writer := csv.NewWriter(&buf)
writer.Comma = '|' // 设置分隔符为管道符
writer.Write([]string{"John", "30", "New York"})
writer.Write([]string{"Jane", "25", "Los Angeles"})
writer.Flush()
fmt.Printf(" %s", buf.String())
// 4. 读取管道符分隔
fmt.Println("\n读取管道符分隔:")
reader = csv.NewReader(strings.NewReader(buf.String()))
reader.Comma = '|'
records, err = reader.ReadAll()
if err != nil {
log.Fatal(err)
}
for _, record := range records {
fmt.Printf(" %v\n", record)
}
}
输出:
=== 自定义分隔符 ===
分号分隔:
[name age city]
[John 30 New York]
[Jane 25 Los Angeles]
制表符分隔 (TSV):
[name age city]
[John 30 New York]
写入管道符分隔:
John|30|New York
Jane|25|Los Angeles
读取管道符分隔:
[John 30 New York]
[Jane 25 Los Angeles]
示例 3:处理特殊字符
package main
import (
"encoding/csv"
"fmt"
"log"
"strings"
)
func main() {
fmt.Println("=== 处理特殊字符 ===\n")
// 1. 包含逗号的字段
fmt.Println("包含逗号的字段:")
data1 := `name,description
"John, Jr.","Works in New York"`
reader := csv.NewReader(strings.NewReader(data1))
records, err := reader.ReadAll()
if err != nil {
log.Fatal(err)
}
for _, record := range records {
fmt.Printf(" %v\n", record)
}
// 2. 包含引号的字段
fmt.Println("\n包含引号的字段:")
data2 := `name,quote
John,"He said ""Hello, World!"""`
reader = csv.NewReader(strings.NewReader(data2))
records, err = reader.ReadAll()
if err != nil {
log.Fatal(err)
}
for _, record := range records {
fmt.Printf(" %v\n", record)
}
// 3. 包含换行符的字段
fmt.Println("\n包含换行符的字段:")
data3 := "name,address\nJohn,\"123 Main St\nApt 4B\""
reader = csv.NewReader(strings.NewReader(data3))
records, err = reader.ReadAll()
if err != nil {
log.Fatal(err)
}
for _, record := range records {
fmt.Printf(" 姓名:%s\n", record[0])
fmt.Printf(" 地址:%s\n", record[1])
}
// 4. 写入特殊字符
fmt.Println("\n写入特殊字符:")
var buf strings.Builder
writer := csv.NewWriter(&buf)
// 写入包含特殊字符的数据
writer.Write([]string{"name", "description"})
writer.Write([]string{"John, Jr.", "Works in \"NYC\""})
writer.Write([]string{"Jane", "Line 1\nLine 2"})
writer.Flush()
fmt.Printf("生成的 CSV:\n%s", buf.String())
// 5. 禁用引号
fmt.Println("\n禁用引号:")
buf.Reset()
writer = csv.NewWriter(&buf)
writer.DisableQuote = true
writer.Write([]string{"John", "No quotes"})
writer.Flush()
fmt.Printf(" %s", buf.String())
}
输出:
=== 处理特殊字符 ===
包含逗号的字段:
[name description]
[John, Jr. Works in New York]
包含引号的字段:
[name quote]
[John He said "Hello, World!"]
包含换行符的字段:
姓名:John
地址:123 Main St
Apt 4B
写入特殊字符:
生成的 CSV:
name,description
"John, Jr.","Works in ""NYC"""
Jane,"Line 1
Line 2"
禁用引号:
John,No quotes
示例 4:注释和前导空格
package main
import (
"encoding/csv"
"fmt"
"log"
"strings"
)
func main() {
fmt.Println("=== 注释和前导空格 ===\n")
// 1. 带注释的 CSV
csvWithComments := `# 这是注释
name,age,city
# 另一条注释
John,30,New York
Jane,25,Los Angeles`
fmt.Println("带注释的 CSV:")
reader := csv.NewReader(strings.NewReader(csvWithComments))
reader.Comment = '#' // 设置注释字符
records, err := reader.ReadAll()
if err != nil {
log.Fatal(err)
}
fmt.Printf("有效记录数:%d\n", len(records))
for _, record := range records {
fmt.Printf(" %v\n", record)
}
// 2. 去除前导空格
fmt.Println("\n去除前导空格:")
csvWithSpaces := `name, age, city
John , 30 , New York
Jane , 25 , Los Angeles `
// 不去除空格
fmt.Println("保留空格:")
reader = csv.NewReader(strings.NewReader(csvWithSpaces))
records, err = reader.ReadAll()
if err != nil {
log.Fatal(err)
}
for i, record := range records {
fmt.Printf(" 行 %d: %v\n", i, record)
}
// 去除前导空格
fmt.Println("\n去除前导空格:")
reader = csv.NewReader(strings.NewReader(csvWithSpaces))
reader.TrimLeadingSpace = true
records, err = reader.ReadAll()
if err != nil {
log.Fatal(err)
}
for i, record := range records {
fmt.Printf(" 行 %d: %v\n", i, record)
}
// 3. 多行注释
fmt.Println("\n多行注释:")
multiCommentCSV := `# 用户数据
# 格式:name,age,city
name,age,city
# 管理员
admin,99,localhost
# 普通用户
user,25,remote`
reader = csv.NewReader(strings.NewReader(multiCommentCSV))
reader.Comment = '#'
records, err = reader.ReadAll()
if err != nil {
log.Fatal(err)
}
fmt.Printf("有效记录数:%d\n", len(records))
for _, record := range records {
fmt.Printf(" %v\n", record)
}
}
输出:
=== 注释和前导空格 ===
带注释的 CSV:
有效记录数:3
[name age city]
[John 30 New York]
[Jane 25 Los Angeles]
去除前导空格:
保留空格:
行 0: [name age city]
行 1: [ John 30 New York ]
行 2: [ Jane 25 Los Angeles ]
去除前导空格:
行 0: [name age city]
行 1: [John 30 New York ]
行 2: [Jane 25 Los Angeles ]
多行注释:
有效记录数:3
[name age city]
[admin 99 localhost]
[user 25 remote]
示例 5:逐行读取大文件
package main
import (
"encoding/csv"
"fmt"
"io"
"log"
"os"
"strings"
)
// User 用户结构
type User struct {
Name string
Age int
City string
Email string
}
// ReadCSVLineByLine 逐行读取 CSV
func ReadCSVLineByLine(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
reader := csv.NewReader(file)
lineNum := 0
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return err
}
lineNum++
fmt.Printf("行 %d: %v\n", lineNum, record)
}
fmt.Printf("总共读取 %d 行\n", lineNum)
return nil
}
// CreateLargeCSV 创建大型 CSV 文件(用于测试)
func CreateLargeCSV(filename string, rows int) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// 写入表头
writer.Write([]string{"id", "name", "value", "description"})
// 写入数据行
for i := 0; i < rows; i++ {
writer.Write([]string{
fmt.Sprintf("%d", i),
fmt.Sprintf("Item %d", i),
fmt.Sprintf("%.2f", float64(i)*1.5),
fmt.Sprintf("Description for item %d", i),
})
}
return nil
}
func main() {
fmt.Println("=== 逐行读取大文件 ===\n")
// 1. 创建测试文件
testFile := "large_test.csv"
fmt.Printf("创建测试文件(1000 行)...\n")
err := CreateLargeCSV(testFile, 1000)
if err != nil {
log.Fatal(err)
}
// 2. 逐行读取
fmt.Println("\n逐行读取(前 10 行):")
file, err := os.Open(testFile)
if err != nil {
log.Fatal(err)
}
defer file.Close()
reader := csv.NewReader(file)
lineNum := 0
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
if lineNum < 10 {
fmt.Printf("行 %d: %v\n", lineNum, record)
}
lineNum++
}
fmt.Printf("\n总共读取 %d 行\n", lineNum)
// 3. 统计信息
fmt.Println("\n统计信息:")
file.Seek(0, 0)
reader = csv.NewReader(file)
// 跳过表头
reader.Read()
total := 0.0
count := 0
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
// 解析 value 列
var value float64
fmt.Sscanf(record[2], "%f", &value)
total += value
count++
}
fmt.Printf("记录数:%d\n", count)
fmt.Printf("总值:%.2f\n", total)
fmt.Printf("平均值:%.2f\n", total/float64(count))
// 清理
os.Remove(testFile)
}
输出:
=== 逐行读取大文件 ===
创建测试文件(1000 行)...
逐行读取(前 10 行):
行 0: [id name value description]
行 1: [0 Item 0 0.00 Description for item 0]
行 2: [1 Item 1 1.50 Description for item 1]
行 3: [2 Item 2 3.00 Description for item 2]
行 4: [3 Item 3 4.50 Description for item 3]
行 5: [4 Item 4 6.00 Description for item 4]
行 6: [5 Item 5 7.50 Description for item 5]
行 7: [6 Item 6 9.00 Description for item 6]
行 8: [7 Item 7 10.50 Description for item 7]
行 9: [8 Item 8 12.00 Description for item 8]
总共读取 1001 行
统计信息:
记录数:1000
总值:749250.00
平均值:749.25
示例 6:CSV 与结构体转换
package main
import (
"encoding/csv"
"fmt"
"log"
"os"
"strconv"
"strings"
)
// Employee 员工结构
type Employee struct {
ID int
Name string
Age int
Department string
Salary float64
}
// EmployeesToCSV 将员工切片转换为 CSV
func EmployeesToCSV(employees []Employee, filename string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// 写入表头
writer.Write([]string{"id", "name", "age", "department", "salary"})
// 写入数据
for _, emp := range employees {
record := []string{
strconv.Itoa(emp.ID),
emp.Name,
strconv.Itoa(emp.Age),
emp.Department,
fmt.Sprintf("%.2f", emp.Salary),
}
writer.Write(record)
}
return nil
}
// CSVToEmployees 从 CSV 读取员工数据
func CSVToEmployees(filename string) ([]Employee, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return nil, err
}
var employees []Employee
// 跳过表头(从索引 1 开始)
for i := 1; i < len(records); i++ {
record := records[i]
id, _ := strconv.Atoi(record[0])
age, _ := strconv.Atoi(record[2])
salary, _ := strconv.ParseFloat(record[4], 64)
emp := Employee{
ID: id,
Name: record[1],
Age: age,
Department: record[3],
Salary: salary,
}
employees = append(employees, emp)
}
return employees, nil
}
// PrintEmployees 打印员工列表
func PrintEmployees(employees []Employee, title string) {
fmt.Printf("=== %s ===\n", title)
fmt.Printf("%-4s %-15s %-4s %-12s %-10s\n", "ID", "Name", "Age", "Department", "Salary")
fmt.Println(strings.Repeat("-", 50))
for _, emp := range employees {
fmt.Printf("%-4d %-15s %-4d %-12s %-10.2f\n",
emp.ID, emp.Name, emp.Age, emp.Department, emp.Salary)
}
fmt.Println()
}
func main() {
fmt.Println("=== CSV 与结构体转换 ===\n")
// 1. 创建员工数据
employees := []Employee{
{ID: 1, Name: "John Doe", Age: 30, Department: "Engineering", Salary: 80000.00},
{ID: 2, Name: "Jane Smith", Age: 25, Department: "Marketing", Salary: 65000.00},
{ID: 3, Name: "Bob Johnson", Age: 35, Department: "Sales", Salary: 75000.00},
{ID: 4, Name: "Alice Brown", Age: 28, Department: "HR", Salary: 60000.00},
{ID: 5, Name: "Charlie Wilson", Age: 42, Department: "Engineering", Salary: 95000.00},
}
PrintEmployees(employees, "原始数据")
// 2. 写入 CSV 文件
csvFile := "employees.csv"
err := EmployeesToCSV(employees, csvFile)
if err != nil {
log.Fatal(err)
}
fmt.Printf("✓ 已写入 %s\n\n", csvFile)
// 3. 从 CSV 读取
loadedEmployees, err := CSVToEmployees(csvFile)
if err != nil {
log.Fatal(err)
}
PrintEmployees(loadedEmployees, "从 CSV 加载")
// 4. 验证数据
fmt.Println("数据验证:")
if len(employees) == len(loadedEmployees) {
fmt.Printf("✓ 记录数匹配:%d\n", len(employees))
}
match := true
for i := range employees {
if employees[i].ID != loadedEmployees[i].ID ||
employees[i].Name != loadedEmployees[i].Name ||
employees[i].Age != loadedEmployees[i].Age ||
employees[i].Department != loadedEmployees[i].Department ||
employees[i].Salary != loadedEmployees[i].Salary {
match = false
break
}
}
if match {
fmt.Println("✓ 所有字段匹配")
}
// 清理
os.Remove(csvFile)
}
输出:
=== CSV 与结构体转换 ===
=== 原始数据 ===
ID Name Age Department Salary
--------------------------------------------------
1 John Doe 30 Engineering 80000.00
2 Jane Smith 25 Marketing 65000.00
3 Bob Johnson 35 Sales 75000.00
4 Alice Brown 28 HR 60000.00
5 Charlie Wilson 42 Engineering 95000.00
✓ 已写入 employees.csv
=== 从 CSV 加载 ===
ID Name Age Department Salary
--------------------------------------------------
1 John Doe 30 Engineering 80000.00
2 Jane Smith 25 Marketing 65000.00
3 Bob Johnson 35 Sales 75000.00
4 Alice Brown 28 HR 60000.00
5 Charlie Wilson 42 Engineering 95000.00
数据验证:
✓ 记录数匹配:4
✓ 所有字段匹配
示例 7:错误处理
package main
import (
"encoding/csv"
"fmt"
"io"
"log"
"strings"
)
func main() {
fmt.Println("=== CSV 错误处理 ===\n")
// 1. 字段数不匹配
fmt.Println("1. 字段数不匹配:")
inconsistentCSV := `name,age,city
John,30,New York
Jane,25`
reader := csv.NewReader(strings.NewReader(inconsistentCSV))
_, err := reader.ReadAll()
if err != nil {
fmt.Printf(" ✗ 错误:%v\n", err)
}
// 允许字段数可变
reader.FieldsPerRecord = -1
records, err := reader.ReadAll()
if err != nil {
fmt.Printf(" ✗ 错误:%v\n", err)
} else {
fmt.Printf(" ✓ 允许字段数可变:%d 条记录\n", len(records))
}
// 2. 未闭合的引号
fmt.Println("\n2. 未闭合的引号:")
unclosedQuoteCSV := `name,quote
John,"He said "Hello""`
reader = csv.NewReader(strings.NewReader(unclosedQuoteCSV))
_, err = reader.ReadAll()
if err != nil {
fmt.Printf(" ✗ 错误:%v\n", err)
}
// 3. 空字段处理
fmt.Println("\n3. 空字段处理:")
emptyFieldsCSV := `name,age,city
John,,New York
,25,
Jane,30,Boston`
reader = csv.NewReader(strings.NewReader(emptyFieldsCSV))
records, err = reader.ReadAll()
if err != nil {
log.Fatal(err)
}
for i, record := range records {
fmt.Printf(" 行 %d: %v (字段数:%d)\n", i, record, len(record))
}
// 4. 空行处理
fmt.Println("\n4. 空行处理:")
emptyLinesCSV := `name,age
John,30
Jane,25
Bob,35`
reader = csv.NewReader(strings.NewReader(emptyLinesCSV))
records, err = reader.ReadAll()
if err != nil {
log.Fatal(err)
}
fmt.Printf(" 读取记录数:%d\n", len(records))
for i, record := range records {
fmt.Printf(" 行 %d: %v\n", i, record)
}
// 5. 读取错误
fmt.Println("\n5. 读取错误:")
// EOF 错误
emptyReader := csv.NewReader(strings.NewReader(""))
_, err = emptyReader.Read()
if err == io.EOF {
fmt.Printf(" ✓ EOF 错误:%v\n", err)
}
// 6. 写入错误
fmt.Println("\n6. 写入错误:")
// 正常写入
var buf strings.Builder
writer := csv.NewWriter(&buf)
err = writer.Write([]string{"John", "30"})
if err != nil {
fmt.Printf(" ✗ 写入错误:%v\n", err)
} else {
fmt.Printf(" ✓ 写入成功\n")
}
writer.Flush()
err = writer.Error()
if err != nil {
fmt.Printf(" ✗ 刷新错误:%v\n", err)
} else {
fmt.Printf(" ✓ 刷新成功:%s", buf.String())
}
// 7. 字段数验证
fmt.Println("\n7. 字段数验证:")
fixedCSV := `name,age,city
John,30,New York
Jane,25,Boston`
reader = csv.NewReader(strings.NewReader(fixedCSV))
reader.FieldsPerRecord = 3 // 固定 3 个字段
records, err = reader.ReadAll()
if err != nil {
fmt.Printf(" ✗ 错误:%v\n", err)
} else {
fmt.Printf(" ✓ 字段数验证通过:%d 条记录\n", len(records))
}
// 测试字段数不匹配
badCSV := `name,age,city
John,30`
reader = csv.NewReader(strings.NewReader(badCSV))
reader.FieldsPerRecord = 3
_, err = reader.ReadAll()
if err != nil {
fmt.Printf(" ✗ 字段数错误:%v\n", err)
}
}
输出:
=== CSV 错误处理 ===
1. 字段数不匹配:
✗ 错误: record on line 3: wrong number of fields
✓ 允许字段数可变:3 条记录
2. 未闭合的引号:
✗ 错误: extraneous or missing " in quoted-field
3. 空字段处理:
行 0: [name age city] (字段数:3)
行 1: [John New York] (字段数:3)
行 2: [ 25 ] (字段数:3)
行 3: [Jane 30 Boston] (字段数:3)
4. 空行处理:
读取记录数:4
行 0: [name age]
行 1: [John 30]
行 2: [Jane 25]
行 3: [Bob 35]
5. 读取错误:
✓ EOF 错误:EOF
6. 写入错误:
✓ 写入成功
✓ 刷新成功:John,30
7. 字段数验证:
✓ 字段数验证通过:2 条记录
✗ 字段数错误:record on line 2: wrong number of fields
最佳实践
✅ 推荐做法
-
总是检查错误
// ✅ 推荐 records, err := reader.ReadAll() if err != nil { return err } writer.Flush() if err := writer.Error(); err != nil { return err } -
大文件使用逐行读取
// ✅ 推荐:大文件 for { record, err := reader.Read() if err == io.EOF { break } // 处理 record } // ❌ 不推荐:大文件可能内存溢出 records, err := reader.ReadAll() -
使用 defer 刷新缓冲区
// ✅ 推荐 writer := csv.NewWriter(file) defer writer.Flush() -
明确设置字段数
// ✅ 推荐:验证字段数 reader.FieldsPerRecord = 3 // 期望 3 个字段 // ✅ 推荐:允许可变 reader.FieldsPerRecord = -1 -
处理特殊字符
// ✅ 推荐:自动处理引号和换行 writer.Write([]string{"John, Jr.", "Works in \"NYC\""})
❌ 不安全做法
-
不要忽略 Flush 错误
// ❌ 错误 writer.Write(record) // 忘记 Flush // ✅ 正确 writer.Write(record) writer.Flush() if err := writer.Error(); err != nil { return err } -
不要假设字段数固定
// ❌ 错误 record := records[0] name := record[0] // 可能 panic age := record[1] // 可能 panic // ✅ 正确 if len(record) < 2 { return fmt.Errorf("字段数不足") } -
不要忽略编码问题
// ❌ 错误:可能是 UTF-16 编码 file, _ := os.Open("data.csv") // ✅ 正确:确保 UTF-8 编码 content, _ := os.ReadFile("data.csv") reader := csv.NewReader(bytes.NewReader(content))
性能优化
批量写入
// ✅ 推荐:批量写入
records := [][]string{
{"John", "30", "New York"},
{"Jane", "25", "Los Angeles"},
// ... 更多数据
}
writer.WriteAll(records)
// ❌ 不推荐:逐条写入(多次 I/O)
for _, record := range records {
writer.Write(record)
}
预分配切片
// ✅ 推荐:预分配
records := make([][]string, 0, expectedRows)
// ❌ 不推荐:动态增长
var records [][]string
使用 bufio 缓冲
// ✅ 推荐:大文件使用缓冲
file, _ := os.Create("output.csv")
defer file.Close()
buf := bufio.NewWriter(file)
writer := csv.NewWriter(buf)
writer.WriteAll(records)
writer.Flush()
buf.Flush()
总结
核心类型
| 类型 | 用途 | 说明 |
|---|---|---|
| Reader | CSV 读取器 | 从输入流读取 CSV |
| Writer | CSV 写入器 | 向输出流写入 CSV |
主要方法
| 方法 | 用途 | 说明 |
|---|---|---|
| Read() | 读取一行 | 返回 []string, error |
| ReadAll() | 读取所有 | 返回 [][]string, error |
| Write() | 写入一行 | 接收 []string |
| WriteAll() | 写入所有 | 接收 [][]string |
| Flush() | 刷新缓冲 | 确保数据写入 |
| Error() | 检查错误 | 返回 Writer 的错误 |
配置选项
| 选项 | Reader | Writer | 说明 |
|---|---|---|---|
| Comma | ✅ | ✅ | 字段分隔符 |
| Comment | ✅ | ❌ | 注释字符 |
| FieldsPerRecord | ✅ | ❌ | 每行字段数 |
| TrimLeadingSpace | ✅ | ❌ | 去除前导空格 |
| Quote | ✅ | ✅ | 引号字符 |
| DisableQuote | ✅ | ✅ | 禁用引号 |
| UseCRLF | ❌ | ✅ | 使用 CRLF 换行 |
| ReuseRecord | ✅ | ❌ | 重用记录切片 |
特殊字符处理
| 字符 | 处理方式 | 示例 |
|---|---|---|
| 逗号 | 引号包围 | "Smith, John" |
| 换行 | 引号包围 | "Line1\nLine2" |
| 引号 | 双引号转义 | "He said ""Hi""" |
| 空格 | 保留原样 | John |
常见用途
| 场景 | 推荐方法 | 说明 |
|---|---|---|
| 小文件 | ReadAll/WriteAll | 一次性读写 |
| 大文件 | Read/Write | 逐行处理 |
| 数据导出 | WriteAll | 批量写入 |
| 数据导入 | Read + 结构体转换 | 解析为对象 |
| 可变字段 | FieldsPerRecord=-1 | 允许字段数不同 |
| 注释支持 | Comment=‘#’ | 跳过注释行 |
参考资料
最后更新:2026-04-03
Go 版本:Go 1.23+