应使用 c.Stream() 配合 json.Encoder 逐个编码元素,手动管理 JSON 数组括号与逗号,避免 c.JSON() 全量序列化;结合数据库游标分页、实时检测客户端断连及反向代理流式配置,实现真正按需流式响应。

响应体过大时如何避免一次性加载全部数据
直接在 echo.Context 中调用 c.JSON() 或 c.Stream() 传入一个大 slice,会触发 Go runtime 将整个结构体序列化为字节流并载入内存——哪怕你只返回前 10 条,后 9990 条也得先生成、再丢弃。这不是“按需”,是“全量加载+部分发送”。
真正按需,得让数据生产者(比如数据库查询、文件读取、外部 API 流)和 HTTP 响应写入器形成流水线,中间不缓存完整结果。
- 用
c.Stream()替代c.JSON(),自己控制每次写入的 chunk - 确保数据源支持迭代(如
sql.Rows、bufio.Scanner、io.Reader),而非一次性rows.Scan()全部进内存 - 注意
echo.HTTPError可能在流中途发生,需在循环内捕获并提前终止
用 c.Stream() 实现分块 JSON 数组流式响应
浏览器或客户端期望接收一个合法 JSON 数组([{},{},...]),但又不想等全部数据就绪。标准做法是手动拼接开头 "["、每个 item 后加逗号、结尾加 "]",但容易出错。更稳妥的是用 json.Encoder 直接写入 c.Response().Writer,它天然支持流式编码。
func streamItems(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
c.Response().Writer.WriteHeader(http.StatusOK)
<pre class="brush:php;toolbar:false;">enc := json.NewEncoder(c.Response().Writer)
if err := enc.Encode([]byte("[")); err != nil {
return err
}
rows, err := db.Query("SELECT id,name FROM users LIMIT 10000")
if err != nil {
return err
}
defer rows.Close()
first := true
for rows.Next() {
var u struct{ ID int; Name string }
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return err
}
if !first {
if _, err := c.Response().Writer.Write([]byte(",")); err != nil {
return err
}
}
first = false
if err := enc.Encode(u); err != nil {
return err
}
}
if err := enc.Encode([]byte("]")); err != nil {
return err
}
return nil}
立即学习“go语言免费学习笔记(深入)”;
关键点:不用 enc.Encode([]interface{}) 整体传入,而是逐个 Encode 单个 struct;手动管理数组括号和逗号,避免 encoder 自动换行/缩进干扰流格式。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
数据库游标分页 + c.Stream() 防止内存爆炸
如果数据来自 MySQL/PostgreSQL,用 LIMIT OFFSET 分页本质仍是全扫描,offset 越大越慢,且服务端仍需加载当前页全部记录到内存。改用游标(cursor-based pagination)配合流式响应,才能真正“按需”:
- 前端传上一页最后一条的
id(或时间戳),SQL 改为WHERE id > ? ORDER BY id LIMIT 100 - 每查出一条就
enc.Encode()一条,不 collect 到 slice - 注意事务隔离级别:若用
READ COMMITTED,流过程中新插入的数据可能被跳过或重复,需根据业务权衡
错误示范:rows, _ := db.Query("SELECT * FROM logs ORDER BY ts DESC LIMIT 10000") —— 这会把 1 万条日志全拉进内存,哪怕只流式写出前 100 条。
客户端断连时如何及时释放资源
c.Stream() 过程中用户关闭连接(如刷新页面、网络中断),c.Response().Writer 会变不可写,但 Go 不会自动停止你的 for 循环。必须主动检测写入是否失败,并退出:
for rows.Next() {
// ... scan ...
if err := enc.Encode(u); err != nil {
// 检查是否因客户端断开导致
if errors.Is(err, io.ErrUnexpectedEOF) ||
errors.Is(err, net.ErrClosed) ||
strings.Contains(err.Error(), "broken pipe") {
return nil // 安静退出,不报错
}
return err
}
}更可靠的方式是监听 c.Request().Context().Done(),尤其在长流场景下,避免 goroutine 泄漏。Echo 的 context 默认继承了请求上下文,可直接用 select 检测取消信号。
最易忽略的一点:流式响应无法设置 Content-Length,所以 Nginx 等反向代理默认会缓冲整个响应再转发,导致延迟。务必在 proxy 配置中启用 proxy_buffering off 或设置 chunked_transfer_encoding on。

















