限速必须作用于上传端读取原始数据的路径,即控制从文件或输入流中“读多少字节”,而非插在 multipart.writer.write 或 http.request.body 写入侧;因后者接口不稳定且易被客户端缓冲、重试绕过。

限速必须作用在上传端读取原始数据路径上
限速不能插在 multipart.Writer.Write 或 http.Request.Body 的写入侧,那些层不暴露稳定接口,且容易被客户端内部缓冲、重试逻辑绕过。真正可控的点只有一个:你从文件或输入流里「读多少字节」。比如用 multipart.Writer 传文件时,限速对象是传给 part.Write() 的那个 io.Reader,不是整个 *os.File 或 http.Request.Body。
常见错误现象:
- 浏览器显示 “upload stalled” 或卡在 99%
- 服务端日志里
Read()调用次数远超预期(比如一个 10MB 文件触发了 50 次Read) - 上传速率忽高忽低,甚至翻倍
原因就是 HTTP 客户端(包括 net/http 和 multipart)会预读、缓冲、重试,多次调用同一个 io.Reader.Read() 方法。如果你的限速逻辑没隔离状态,每次调用都会扣令牌,速率就失控了。
必须用 io.Pipe + goroutine 隔离限速 Reader 状态
直接把带 rate.Limiter 的 io.Reader 当作 http.Request.Body 传进去,一定会出问题。正确做法是启动一个 goroutine,从限速 Reader 读,往 io.PipeWriter 写;HTTP 请求体用 io.PipeReader。
关键点:
-
io.PipeWriter.Close()必须由写端显式调用,否则读端永远阻塞 - goroutine 中要
recoverpanic 后也close(writer),避免连接泄漏 - 大文件(>100MB)不用管道,
multipart内部 buffer 可能吃光内存 - 别在 goroutine 里异步读 + channel 传 —— 容易丢字节或死锁
rate.Limiter.WaitN 必须配对 ReserveN + Wait
只调 limiter.ReserveN(ctx, n) 不会阻塞,也不消耗令牌;它只是返回一个 *rate.Reservation,漏掉 res.Wait(ctx) 就等于没限速。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
典型错误写法:
res := limiter.ReserveN(ctx, len(p))
if !res.OK() { /* ... */ }
// ❌ 缺少 res.Wait(ctx),限速失效
正确姿势:
- 每次
Read()前都做res := limiter.ReserveN(ctx, len(p)),然后立刻res.Wait(ctx) - 初始化
burst至少等于limit(例如 1MB/s →burst = 1024 * 1024),否则可能 panic 或拒绝合法重试 - 别用
rate.Every(time.Second / bps),Go 编译器可能优化掉除法;应写rate.Limit(1024 * 1024) -
ctx被 cancel 时Wait返回 error,此时应立即停止 goroutine 并close(writer)
别用 io.LimitReader 实现“每秒 N 字节”
io.LimitReader(file, n) 只限制总字节数,和时间无关。传 io.LimitReader(f, 1024*1024) 表示“最多读 1MB 后返回 io.EOF”,不是“每秒读 1MB”。上传中途就断掉,客户端看到的是不完整数据或连接重置。
常见错误现象:
- 浏览器下载提示 “corrupted content” 或 “network error”
- HTTP Range 请求失效(比如请求最后 1MB,却从头开始限总长)
- 上传时客户端卡在 99%,实际服务端早已读完但没发完
真正要控速率,就得用 rate.Limiter 控制每次 Read 所读的字节数,并且必须和真实读取量对齐——也就是封装一个支持 Read/Seek/Stat 的限速 io.ReadSeeker,否则 http.ServeContent 无法处理 Range。
最易被忽略的点:限速粒度。单次 WaitN 不宜过大(比如一次等 1MB),应拆成 4–64KB 小块。太大会导致响应延迟突增,太小则系统调用开销上升。还有,rate.Limiter 实例不能每个请求都新建,得按用户、租户或路径复用,否则限流失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










