rate.limiter限速生效必须配对reserven与wait,仅调用reserven不阻塞也不扣令牌;burst应≥limit;需用io.pipe+goroutine隔离状态防重试干扰;不可用io.limitreader控速。

rate.Limiter 必须配对 ReserveN + Wait 才生效
限速失效最常见的原因是只调用了 limiter.ReserveN(ctx, n) 却漏掉 res.Wait(ctx)。ReserveN 只是预检,返回一个 *rate.Reservation,不阻塞、不扣令牌;Wait 才真正按速率放行。漏掉这步,上传会全速跑满带宽。
典型错误写法:
res := limiter.ReserveN(ctx, 1024)
if !res.OK() { /* 处理失败 */ } // ❌ 没 Wait,限速形同虚设
正确写法必须每次读前都做:
res := limiter.ReserveN(ctx, n)- 立刻
res.Wait(ctx),不能延迟或条件跳过 - 如果
ctx被 cancel,Wait返回 error,应立即 break 并 closeio.PipeWriter - burst 初始化至少等于 limit(例如限速 1MB/s,
burst = 1024 * 1024),否则合法重试可能被拒绝
限速 Reader 不能直接传给 http.Request.Body
HTTP 客户端(包括 http.Post、multipart.Writer)内部会多次调用 Read —— 预读、缓冲、重试都可能导致限速逻辑被重复触发,实际速率翻倍甚至卡死在 99%。
常见现象:multipart.Writer 反复调用你的限速 Reader 的 Read,服务端日志里 Read 调用次数远超文件大小 / chunk 数,CPU 却不高(大量 goroutine 阻塞等待令牌)。
必须用 io.Pipe + goroutine 隔离状态:
- 启动一个 goroutine,从限速 Reader 读,往
io.PipeWriter写 -
http.Request.Body设为io.PipeReader -
io.PipeWriter.Close()必须由写端显式调用,否则读端永远阻塞 - recover panic 后也要 close writer,避免连接泄漏
别用 io.LimitReader 实现“字节/秒”限速
io.LimitReader(r, n) 只限制总字节数,不是速率。比如 io.LimitReader(file, 1024*1024) 表示“最多读 1MB 就返回 io.EOF”,和时间完全无关 —— 上传中途就断掉,客户端看到的是不完整数据或连接重置。
如果你真想用它,只能配合 io.Pipe + goroutine 做“流式截断”,但依然无法控速。要实现精确的 “B/s” 限速,唯一可靠路径是 rate.Limiter 控制每次 Read 的时机与字节数。
注意初始化写法:
- ❌
rate.Every(time.Second / float64(bps))—— Go 编译器可能优化掉除法,导致间隔错误 - ✅
rate.Limit(bps)(bps 是字节/秒,不是 bit/秒)
大文件 (>100MB) 不加管道会吃光内存
multipart 或 http.Client 内部 buffer 可能预读整个文件,尤其当使用 multipart.Writer 时,没 io.Pipe 隔离会导致内存占用飙升。这不是理论风险,实测 >100MB 文件在无管道场景下常触发 OOM。
上传场景下,限速对象必须是「读取侧」:即原始文件流(os.File 或 bufio.Reader)套上 rate 控制后,再进 pipe;而不是试图包装 http.Request.Body.WriteTo 或拦截 net.Conn.Write —— 这些层不暴露稳定接口,且容易被 client 内部逻辑绕过。
如果你用 multipart.Writer,限速 Reader 应传给 part.Write() 的参数,不是整个 file 句柄。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











