当前位置:首页 > 文章列表 > Golang > Go问答 > Go 读取 CSV 入库时如何处理 BOM、空行和重复记录?

Go 读取 CSV 入库时如何处理 BOM、空行和重复记录?

来源:17golang原创 2026-07-27 16:21:20 0浏览 收藏

客户名单从 Excel 导出成 CSV 后,最容易出问题的不是数据库连接,而是文件第一列带着不可见的 UTF-8 BOM、末尾混着空行,或者同一个客户在文件里出现两次。Go 的 encoding/csv 能稳定读取这些行,但“读出来”不等于“可以入库”:要在写入 customer_import 前完成字段清洗、列数校验和重复策略确认。

一套可靠的 CSV 导入流程应该把输入行变成“可定位、可校验、可回滚”的记录:首行去 BOM,空行跳过,坏行记录行号,重复客户交给唯一键和明确的业务策略处理。

实践要点
  • 只在第一列第一次读取时移除 UTF-8 BOM,不要对整行做粗暴替换。
  • 先检查列数和必填字段,再做数据库写入,错误行保留 CSV 行号。
  • customer_no 建唯一索引,重复记录选择跳过或更新,不能静默覆盖。
  • 导入使用事务和分批提交,失败时能回滚并输出最后处理行。

一行 CSV 到一条数据库记录,中间缺了哪些环节

把 CSV 当成“按逗号切字符串”会很快踩坑。字段里可能有逗号,双引号还会改变字段边界;所以读取层交给 csv.Reader,业务层再负责规范化。本文示例使用这样的文件:

customer_no,name,email
C001,"上海,一号店",shop@example.com

C001,一号店,shop@example.com
C002, ,bad-email

最终要得到的是:合法行进入 customer_import,重复的 C001 被识别,空白姓名和错误邮箱进入错误清单,而不是让整批导入在一个模糊的数据库报错处停住。

Go encoding/csv 读取客户 CSV,首列去除 UTF-8 BOM 并跳过空行后进入字段校验

先处理 BOM、空行和列数,再谈字段校验

UTF-8 BOM 通常只出现在文件开头。它会让第一列表头变成 \ufeffcustomer_no,如果代码按表头查找,就会误以为文件缺少 customer_no。最稳妥的办法是在第一行的第一个字段上做一次性处理,同时打开 FieldsPerRecord 让列数错误尽早暴露。

func readRows(r io.Reader) ([][]string, error) {
    cr := csv.NewReader(bufio.NewReader(r))
    cr.FieldsPerRecord = 3
    cr.TrimLeadingSpace = true

    header, err := cr.Read()
    if err != nil {
        return nil, fmt.Errorf("read header: %w", err)
    }
    if len(header) > 0 {
        header[0] = strings.TrimPrefix(header[0], "\ufeff")
    }
    if !reflect.DeepEqual(header, []string{"customer_no", "name", "email"}) {
        return nil, fmt.Errorf("unexpected header: %v", header)
    }

    var rows [][]string
    for {
        row, err := cr.Read()
        if errors.Is(err, io.EOF) {
            break
        }
        if err != nil {
            return rows, fmt.Errorf("read line %d: %w", cr.InputOffset(), err)
        }
        if allBlank(row) {
            continue
        }
        rows = append(rows, row)
    }
    return rows, nil
}

func allBlank(row []string) bool {
    for _, value := range row {
        if strings.TrimSpace(value) != "" {
            return false
        }
    }
    return true
}

这里的 TrimLeadingSpace 只处理未被引号保护的字段前空格,不能代替业务清洗。比如姓名要用 strings.TrimSpace,邮箱还需要单独验证。

把规范化和错误行保存下来,导入才可复查

建议把“原始行号”一直带到校验结果里。不要只返回一个 invalid data,否则运营人员无法在原 CSV 中定位。规范化函数可以只做确定性动作:去掉首尾空白、把空字符串识别为缺失、统一邮箱的小写。

type CustomerRow struct {
    Line       int
    CustomerNo string
    Name       string
    Email      string
}

type RowError struct {
    Line   int
    Reason string
}

func normalize(line int, row []string) (CustomerRow, *RowError) {
    item := CustomerRow{
        Line:       line,
        CustomerNo: strings.TrimSpace(row[0]),
        Name:       strings.TrimSpace(row[1]),
        Email:      strings.ToLower(strings.TrimSpace(row[2])),
    }
    switch {
    case item.CustomerNo == "":
        return item, &RowError{line, "customer_no 为空"}
    case item.Name == "":
        return item, &RowError{line, "name 为空"}
    case !strings.Contains(item.Email, "@"):
        return item, &RowError{line, "email 格式不正确"}
    default:
        return item, nil
    }
}

真实项目里,邮箱校验可以换成更严格的规则;但不要为了“校验很完整”引入一套与业务不匹配的正则。导入程序首先要能给出稳定、可解释的错误。

Go CSV 导入在字段校验后按 customer_no 分流,合法记录入库,重复记录进入处理分支

用 customer_no 唯一键兜住重复记录

内存里的 map[string]struct{} 可以发现同一文件内的重复,但它看不到数据库中已经存在的客户。最终边界必须由数据库唯一索引承担:

CREATE TABLE customer_import (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    customer_no VARCHAR(32) NOT NULL,
    name VARCHAR(120) NOT NULL,
    email VARCHAR(160) NOT NULL,
    created_at DATETIME NOT NULL,
    UNIQUE KEY uk_customer_no (customer_no)
);

重复策略要先问清楚业务。名单是“首次导入快照”时,重复行可以记录为 skipped;客户资料允许同步时,可以使用数据库方言提供的 upsert。不要把重复当成异常直接吞掉,也不要默认用最后一行覆盖前一行。

重复位置建议处理核对方式
同一 CSV 内保留首条并记录行号,或合并前先报错map 计数
CSV 与数据库之间唯一键拦截,再按业务选择跳过/更新受影响行数
重跑同一批文件批次号或导入指纹幂等import_batch 表

事务和分批提交:让失败可回滚、进度可追踪

小文件可以一个事务完成;较大的客户名单适合每 500 或 1000 行提交一次。每批提交前记录起止行号,遇到数据库错误时回滚当前批次,并把批次号、最后成功行和错误信息写入日志。这里别急着把提交粒度调得很大,事务越大,锁和重试成本也越难控制。

tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()

stmt, err := tx.PrepareContext(ctx, `
    INSERT INTO customer_import (customer_no, name, email, created_at)
    VALUES (?, ?, ?, NOW())`)
if err != nil { return err }
defer stmt.Close()

for _, item := range validRows {
    if err := insertCustomer(ctx, stmt, item); err != nil {
        return fmt.Errorf("line %d: insert customer_no=%s: %w", item.Line, item.CustomerNo, err)
    }
}
return tx.Commit()

上面的示例展示的是“重复即失败”的保守策略。若选择跳过或更新,应把 SQL 和统计字段一起调整,例如分别统计 insertedskippedupdated,这样导入结束后才知道文件实际发生了什么。

insertCustomer 是示例中的业务封装函数,内部调用数据库驱动的参数化写入方法;把这层封装留在项目里,后续切换“跳过重复”或“更新重复”时,事务循环和错误统计都不用重写。

常见问题:CSV 导入为什么看似成功却少了数据

为什么第一列会多出一串看不见的字符?

多数情况是 UTF-8 BOM。只对首行首列做 strings.TrimPrefix(value, "\ufeff"),不要把所有字段的内容都改写。

空行应该报错还是跳过?

纯空行通常可以跳过;如果一行只有部分字段,应该保留行号并进入错误清单,因为它可能代表被截断的记录。

只用 Go 的 map 去重够不够?

不够。map 只能覆盖当前文件,数据库唯一索引才能覆盖并发导入和重复重跑。

导入失败后可以直接重新上传吗?

先确认事务是否回滚、哪些批次已经提交,再根据批次号或导入指纹重跑。没有幂等设计时,直接重跑可能制造新的重复记录。

最后检查这四项,导入结果才算完成

  • 抽查第一列表头,确认 BOM 没有进入字段名。
  • 统计原始行、空行、错误行、合法行和重复行,数字能对上。
  • 查询 uk_customer_no,确认数据库中没有重复客户号。
  • 保留导入批次号和失败行号,确保同一文件可以安全复查或回滚。

CSV 导入的难点不在 for 循环,而在边界是否被记录清楚:输入怎么清洗、坏行怎么定位、重复如何裁决、失败怎样恢复。把这些状态显式化后,Go 的标准库就足够支撑一条小而稳的导入链路。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go range 遍历 map 时删除元素安全吗:遍历语义、随机顺序与测试边界Go range 遍历 map 时删除元素安全吗:遍历语义、随机顺序与测试边界
上一篇
Go range 遍历 map 时删除元素安全吗:遍历语义、随机顺序与测试边界
Python Decimal 金额为什么多出 0.01:quantize、ROUND_HALF_UP 与浮点输入排查
下一篇
Python Decimal 金额为什么多出 0.01:quantize、ROUND_HALF_UP 与浮点输入排查
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    97次使用
  • OpenCompass大模型评测体系详解:功能、使用指南与应用场景
    OpenCompass
    OpenCompass是上海AI实验室推出的开源大模型评测平台,提供CompassKit、CompassHub和CompassRank三大核心组件,支持LLM及多模态模型的一站式标准化评估与排行榜查询。
    27次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    252次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    179次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    111次使用