go做etl的核心优势是用goroutine+channel构建流式流水线,分extract、transform、load三阶段异步处理,避免全量加载导致内存溢出或卡死。

用 goroutine + channel 搭建流水线,别直接 for-range 处理全量数据
Go 做 ETL 的核心优势不是语法糖,而是能用极轻量的方式把 Extract、Transform、Load 拆成可调度的阶段。很多人一上来就 for range 读完所有记录再统一转换,结果内存爆掉或卡死在某条脏数据上。
正确做法是让每个阶段通过 chan 流式传递单条或小批量数据,靠 goroutine 控制并发节奏:
- Extractor 启动多个 goroutine 并发读不同文件/分片,每读一行往
inCh chan string发送 - Transformer 起固定数量 worker(比如
runtime.NumCPU()),从inCh取数据、处理、发到outCh chan Person - Loader 从
outCh收数据,攒够 100 条再批量插入,避免单条INSERT网络开销过大
这样哪怕源数据有 10GB,内存常驻也只占几百 KB——关键在“流”,不在“批”。
数据库加载时别用 db.Exec 单条插入,小心 context deadline exceeded
用 db.Exec 循环插万条数据,大概率触发连接超时或目标库限流。PostgreSQL 默认 statement_timeout=60s,MySQL 的 wait_timeout 也常设为 300s,而单条插入在高延迟链路下很容易超。
实操建议:
- 优先走
pgx.Batch(PostgreSQL)或mysql.NewStmt预编译 +stmt.Execute批量执行 - 对 SQLite 这类嵌入式库,必须显式开启事务:
tx, _ := db.Begin(),否则每条都是独立事务,性能跌 10 倍以上 - 目标表提前建好索引?错。ETL 加载阶段先删索引,等数据写完再重建,速度提升常达 3–5 倍
Transformer 里别用全局 map 存维表,goroutine 并发写会 panic: assignment to entry in nil map
常见错误:在 main 里声明 var dimMap map[string]int,然后多个 Transformer goroutine 直接往里写——Go 不允许并发写未初始化的 map,运行时直接 panic。
真正安全的做法只有两种:
- 启动前一次性加载维表到
sync.Map或普通map+sync.RWMutex保护读写 - 更推荐「无状态」:把维表封装成函数,每次调用查缓存(如
github.com/bluele/gcache),不共享可变状态 - 绝对不要在 transformer 函数里做
http.Get查外部 API——超时、重试、连接池都得自己管,容易拖垮整条流水线
增量同步别靠 SELECT * FROM A EXCEPT SELECT * FROM B,大表直接 OOM
有人想用 SQL 做全字段对比实现增量,但 EXCEPT 或 NOT EXISTS 在千万级表上会触发全表扫描+临时磁盘排序,内存占用飙升,etl-engine 的增量模块也是避开这条路的。
工业级做法是依赖业务主键 + 时间戳/版本号:
- 源表必须有
updated_at或version字段,ETL 任务记录上次最大值,下次只拉WHERE updated_at > ? - 如果源库不支持更新时间,退一步用自增 ID 分段:
WHERE id BETWEEN ? AND ?,配合SELECT MAX(id)动态切片 - 对比差异时,永远只比主键和关键字段哈希(如
MD5(CONCAT(col1,col2))),不比原始值——省内存、快 10 倍以上
真正的难点从来不在“怎么写代码”,而在“怎么定义哪条算新、哪条算改、哪条该删”——这得跟业务方对着字段一条条对齐,不是技术能绕过去的。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











