db.Find(&results) 会 OOM 是因为 GORM 默认一次性将全部查询结果加载进内存,例如 100 万行、每行 2KB 就占用 2GB;Gin 的 c.JSON() 等方法还会缓冲整个响应体,不流式 flush,导致内存积压和 goroutine 泄漏。

为什么直接用 db.Find(&results) 会 OOM?
因为 Gin 的 HTTP handler 是单 goroutine 处理请求,而 gorm.DB.Find() 默认一次性把全部记录加载进内存——哪怕你只查 100 万行、每行 2KB,也要占 2GB 内存。更糟的是,Gin 默认响应体缓冲区不流式 flush,整个 []byte 会卡在内存里等写完才发,中间任何环节失败(如客户端断连、超时)都会导致 goroutine 泄漏。
怎么让 GORM 分批查 + Gin 流式返回?
核心是绕过 GORM 的全量加载机制,改用 Rows() 手动迭代,并配合 c.Writer 直接写响应流。注意三点:
-
Rows()返回的是底层*sql.Rows,必须显式调用rows.Close(),否则连接不会释放 - Gin 的
c.Writer不支持 chunked encoding 自动分块,需手动控制 write + flush - 不能用
c.JSON()或c.Render(),它们会 buffer 整个响应
示例片段:
rows, err := db.Table("orders").Where("created_at > ?", time.Now().AddDate(0, 0, -7)).Rows()
if err != nil {
c.AbortWithStatusJSON(500, gin.H{"error": err.Error()})
return
}
defer rows.Close()
<p>c.Header("Content-Type", "application/json")
c.Header("X-Content-Type-Options", "nosniff")</p><p>// 写入 JSON 数组开头
c.Writer.Write([]byte("[\n"))</p><p>for rows.Next() {
var order Order
if err := db.ScanRows(rows, &order); err != nil {
c.AbortWithStatusJSON(500, gin.H{"error": err.Error()})
return
}</p><pre class="brush:php;toolbar:false;">// 序列化单条并写入
data, _ := json.Marshal(order)
c.Writer.Write(data)
// 判断是否还有下一条,决定加逗号
if rows.Next() {
c.Writer.Write([]byte(",\n"))
rows.Close() // 注意:这里不能 close,应移到循环外
break
}
rows.Next() // 错误示范!实际应靠 rows.Err() 和计数判断}
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
// 正确做法:用布尔标记是否首条,或提前 count 总数 c.Writer.Write([]byte("\n]")) c.Writer.Flush()
如何避免 rows.Next() 多调用导致 panic?
rows.Next() 每次移动游标并准备读取下一行,调用两次会跳过数据,且第二次可能返回 false 但不报错,导致漏数据或 panic(如 scan 时字段数不匹配)。正确模式是:
- 用
for rows.Next() { ... }循环主体,内部只调一次rows.Next() - 循环结束后检查
rows.Err(),捕获扫描错误(比如类型不匹配) - 不要在循环体内再调
rows.Next()判断“是否有下一条”——那是循环条件干的事
安全写法:
first := true
for rows.Next() {
var item MyModel
if err := db.ScanRows(rows, &item); err != nil {
log.Printf("scan error: %v", err)
break
}
<pre class="brush:php;toolbar:false;">if !first {
c.Writer.Write([]byte(",\n"))
}
first = false
data, _ := json.Marshal(item)
c.Writer.Write(data)} if err := rows.Err(); err != nil { c.AbortWithStatusJSON(500, gin.H{"error": "db scan failed"}) return } c.Writer.Write([]byte("\n]")) c.Writer.Flush()
流式响应下怎么处理超时和客户端断连?
Gin 默认不检测客户端是否已断开,c.Writer.Write() 成功不代表数据真发出去了。必须主动检查:
- 每次
c.Writer.Flush()后调用c.Writer.Available()—— 如果返回 false,说明底层连接已关闭 - 设置
c.Writer.Timeout()(需自定义 writer 包装器)不如用http.Request.Context().Done()更可靠 - 推荐在循环内定期检查:
select { case
简单防御:
for rows.Next() {
select {
case <-c.Request.Context().Done():
log.Println("client disconnected")
return
default:
}
<pre class="brush:php;toolbar:false;">// ... 处理单行
c.Writer.Write(...)
c.Writer.Flush()
// 检查写状态
if !c.Writer.Available() {
return
}}
真正的大批量导出场景,建议加限速(如 time.Sleep(10 * time.Millisecond)),避免压垮下游或触发反爬策略。数据库游标本身不阻塞,但频繁 flush 对小包网络反而更耗资源——这个平衡点得实测。

















