Go标准I/O限速必须用字节级令牌桶精确控制吞吐,否则导致截断、卡死或单位错位;限速Reader需实现io.ReadCloser接口以避免fd泄漏;下载限速用rate.Limiter.WaitN按实际读取字节数申请令牌;上传限速需io.Pipe+goroutine隔离状态;服务端响应限速应改用juju/ratelimit。

Go 标准 I/O 限速不能靠 io.LimitReader 或简单 time.Sleep,必须用字节级令牌桶在 Read / Write 调用路径上精确控制吞吐——否则不是限速,是截断、卡死或单位错位。
限速 Reader 必须实现 io.ReadCloser 接口
HTTP 响应体、文件句柄等都是 io.ReadCloser,你包装后也得保持该接口,否则 defer resp.Body.Close() 会漏关原始连接,导致 fd 泄漏或连接复用失败。
- 自定义结构体要同时实现
Read(p []byte) (int, error)和Close() error -
Close()方法里必须调用原始 reader 的Close(),不能只停 ticker 或 close buffer - 别返回裸指针如
&limitedReader{...}却没实现Close——Go 不会自动补全,运行时关不掉底层资源 - 示例中常见错误:用
io.LimitReader(resp.Body, n)直接传给io.Copy,它只实现Read,没有Close,resp.Body永远不被释放
下载限速要用 rate.Limiter.WaitN 按实际读取字节数申请令牌
限速的核心是「每秒多少字节」,不是「每秒多少次 Read 调用」。标准库 golang.org/x/time/rate.Limiter 默认按事件计数,必须显式用字节数驱动。
- 初始化:用
rate.NewLimiter(rate.Limit(512*1024), 512*1024)(512KB/s),别写rate.Every(time.Second / 512000)—— Go 编译器可能优化掉除法,导致速率失控 - 每次
Read返回n字节后,立刻调limiter.WaitN(ctx, n);若用ReserveN,必须检查res.OK()并手动time.Sleep(res.Delay()) - 不能预估字节数(比如固定传
WaitN(ctx, 4096)),网络底层可能返回更少,必须用真实n - 如果
ctx被 cancel,WaitN返回 error,应立即停止读取并 cleanup,比如关闭io.PipeWriter
上传限速必须加 io.Pipe + goroutine 隔离状态
HTTP 客户端(尤其 multipart 场景)可能多次调用 Read,比如预读、重试、分块缓冲——直接把限速 Reader 当作 http.Request.Body 会导致令牌被重复消耗,实际速率翻倍甚至阻塞。
- 正确结构:起一个 goroutine,从限速
Reader读 → 写入io.PipeWriter;http.Request.Body设为io.PipeReader -
io.PipeWriter.Close()必须由写端触发,否则读端永远阻塞;别在 goroutine 外提前 close - 别把限速逻辑塞进
multipart.Writer的Write里——它内部有 buffer,会绕过你的节流点 - 上传场景下,
rate.Limiter仍可用,但必须确保每个上传请求独占一个 limiter 实例,避免多个请求共用桶导致互相干扰
服务端响应限速必须换 github.com/juju/ratelimit
golang.org/x/time/rate.Limiter 的 Burst 是「次数」,不是字节,用于 response 写入会严重错位:设 rate.NewLimiter(rate.Limit(1024*1024), 1024*1024) 表示「每秒允许百万次 Write 调用」,而非「每秒 1MB」。
- 改用
github.com/juju/ratelimit:bucket := ratelimit.NewBucketWithRate(1024*1024, 1024*1024),第一个参数就是字节/秒,语义清晰 - 必须先调
w.WriteHeader(statusCode)再包装ResponseWriter,否则自动写 header 可能覆盖状态码 - gzip 中间件会绕过限速——要在压缩之后再包装,即包装
gzipResponseWriter,而不是原始ResponseWriter - 务必设置
Content-Length,否则 chunked 编码会让限速不可控;同时 handler 入口加context.WithTimeout,并在每次Write前检查ctx.Err()
真正难的不是写限速逻辑,而是判断限速点落在哪一层:下载限速在 Response.Body → io.Writer 之间,上传限速在 io.Reader → Request.Body 之前,服务端响应限速在 handler.Write → TCP write 之间——错一层,就变成假限速。

















