Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 标准

基本规则

  1. 每行一条记录,以 CRLF(\r\n)或 LF(\n)结尾
  2. 字段之间用逗号分隔
  3. 字段可以包含或不包含引号
  4. 如果字段包含以下字符,必须用引号包围:
    • 逗号(,)
    • 换行符(\n 或 \r\n)
    • 双引号(“)
  5. 引号内的双引号用两个双引号表示(“”)

示例

# 普通字段
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

最佳实践

✅ 推荐做法

  1. 总是检查错误

    // ✅ 推荐
    records, err := reader.ReadAll()
    if err != nil {
        return err
    }
    
    writer.Flush()
    if err := writer.Error(); err != nil {
        return err
    }
    
  2. 大文件使用逐行读取

    // ✅ 推荐:大文件
    for {
        record, err := reader.Read()
        if err == io.EOF {
            break
        }
        // 处理 record
    }
    
    // ❌ 不推荐:大文件可能内存溢出
    records, err := reader.ReadAll()
    
  3. 使用 defer 刷新缓冲区

    // ✅ 推荐
    writer := csv.NewWriter(file)
    defer writer.Flush()
    
  4. 明确设置字段数

    // ✅ 推荐:验证字段数
    reader.FieldsPerRecord = 3  // 期望 3 个字段
    
    // ✅ 推荐:允许可变
    reader.FieldsPerRecord = -1
    
  5. 处理特殊字符

    // ✅ 推荐:自动处理引号和换行
    writer.Write([]string{"John, Jr.", "Works in \"NYC\""})
    

❌ 不安全做法

  1. 不要忽略 Flush 错误

    // ❌ 错误
    writer.Write(record)
    // 忘记 Flush
    
    // ✅ 正确
    writer.Write(record)
    writer.Flush()
    if err := writer.Error(); err != nil {
        return err
    }
    
  2. 不要假设字段数固定

    // ❌ 错误
    record := records[0]
    name := record[0]  // 可能 panic
    age := record[1]   // 可能 panic
    
    // ✅ 正确
    if len(record) < 2 {
        return fmt.Errorf("字段数不足")
    }
    
  3. 不要忽略编码问题

    // ❌ 错误:可能是 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()

总结

核心类型

类型用途说明
ReaderCSV 读取器从输入流读取 CSV
WriterCSV 写入器向输出流写入 CSV

主要方法

方法用途说明
Read()读取一行返回 []string, error
ReadAll()读取所有返回 [][]string, error
Write()写入一行接收 []string
WriteAll()写入所有接收 [][]string
Flush()刷新缓冲确保数据写入
Error()检查错误返回 Writer 的错误

配置选项

选项ReaderWriter说明
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+