GORM 默认会将CLOB字段全量加载至内存导致OOM,需绕过其映射层改用database/sql.Rows配合驱动特有接口(如mysql.TextReader或达梦LOB句柄)流式读取。

Go 用 GORM 直接查 CLOB 字段(比如 MySQL 的 LONGTEXT、Oracle/DM8 的 CLOB)时,默认会一次性加载全部内容到内存,字段超大(几十 MB)就容易 OOM 或拖慢整个查询。这不是 GORM 的 bug,而是它默认按“完整值映射”设计的必然行为——你得主动绕开。
为什么 GORM 默认不支持 CLOB 流式读取
GORM 的核心模型是结构体字段与数据库列一一映射,所有字段都走 Scan 接口。而标准 database/sql 对 sql.NullString 或 string 类型的 Scan,底层调用的是 Rows.Scan,它必须把整段数据读进内存再赋值。CLOB/BLOB 没有原生流式类型支持,GORM 也没封装出类似 io.Reader 的字段类型。
-
string、*string、sql.NullString字段都会触发全量加载 - 即使你只 SELECT CLOB 字段本身,GORM 仍会为该字段分配完整内存空间
-
Find、First、Raw查询中若包含 CLOB 列,只要结果集被解码(即调用了Scan),就会加载全文
绕过 GORM 映射,用 database/sql 原生 Rows 处理 CLOB
真正能流式读 CLOB 的,只有 database/sql.Rows 配合驱动特定的类型(如 MySQL 驱动的 mysql.TextReader,达梦 DM8 驱动的 sql.Scanner 实现)。GORM 的 Session 或 Raw 可以拿到底层 *sql.Rows,这是唯一可控入口。
- 用
db.Raw("SELECT id, content FROM docs WHERE id = ?", id).Rows()获取*sql.Rows - 调用
rows.Next()后,对 CLOB 列用rows.Scan(&id, &contentReader),其中contentReader是实现了sql.Scanner的自定义类型(例如包装io.ReadCloser) - MySQL 场景下可直接用
mysql.TextReader(需导入gorm.io/driver/mysql包);达梦 DM8 需确认其 Go 驱动是否暴露了类似接口,否则要手动调rows.ColumnTypes()+rows.Scan分块读 - 别忘了
defer rows.Close(),否则连接不会释放
在 GORM Model 中保留 CLOB 字段但禁止自动加载
如果业务上仍想用 GORM 的结构体建模,又不想每次查都带 CLOB,可以用字段标签控制加载时机。GORM 不支持“懒加载字段”,但可以靠 SELECT 显式排除。
- 给 CLOB 字段加
gorm:":false"标签,禁用读写映射(避免误赋值) - 定义两个结构体:
DocMeta(不含 CLOB,用于列表/摘要)和DocFull(含 CLOB,仅用于详情流式读) - 永远用
SELECT id, title, updated_at FROM docs查列表,CLOB 字段绝不出现在 SELECT 列表里 - 详情页改用
Raw().Rows()方案,而不是First(&doc)
达梦 DM8 下 CLOB 流读的特别注意点
达梦官方 Go 驱动(github.com/dmhsu/dmgo 或国产适配版)对 CLOB 支持较弱:它不返回 io.Reader,也不实现标准 sql.Scanner 的流式逻辑。实际测试中,Scan 仍会尝试把整个 CLOB 加载为 []byte。
- 必须启用达梦的 LOB 定位器模式(connection string 加
lobLocator=true参数) - 查询后不能直接
Scan到string,而应 Scan 到*dmgo.Lob(或对应驱动中的 LOB 句柄类型) - 再调用该句柄的
ReadAt或OpenReader()方法分块读取,否则仍会内存爆炸 - 务必检查你用的达梦驱动版本是否公开了
Lob类型 —— 很多企业定制版驱动会删减这部分 API
流式读 CLOB 的本质不是“让 GORM 支持它”,而是“及时跳出 GORM 的映射层”。最稳的路径永远是:用 GORM 做元数据操作,用原生 database/sql + 驱动能力做大字段处理。别指望一个 ORM 自动帮你管好几百 MB 的文本流。


















