必须在TCP或HTTP层用rate.Limiter包装net.Conn或io.Reader/Writer实现流量整形,读写需独立限速,协同超时设置,并避免破坏协议语义与引发内存问题。

直接在 TCP 层或 HTTP 层做流量整形,必须绕开 http.FileServer 和 io.Copy 默认行为——它们不感知速率,也不可控。核心手段是用 rate.Limiter 包装 net.Conn 或自定义 io.Reader/io.Writer,再配合流式缓冲与超时协调。
用 rate.Limiter 包装 net.Conn 实现 TCP 层双向限速
适用于 Redis/PostgreSQL 代理、TCP 转发器等非 HTTP 场景,不依赖协议解析,纯字节流控。
- 读写必须用两个独立的
rate.Limiter:读限速控制接收(如客户端下载),写限速控制发送(如服务端上传) -
Write(p []byte)前调用writeLimiter.WaitN(ctx, len(p));若len(p) > 64*1024,应分块调用,避免单次等待过久阻塞 -
Read(p []byte)不能直接对整块p调用WaitN——实际只读到 2 字节却等了 1MB 配额,会卡死;应按真实读取长度等额等待 -
SetReadDeadline和限速必须协同:若限速太慢,deadline 可能先触发超时,建议将 deadline 设为 limiter 预估等待时间的 2–3 倍
HTTP 大文件下载时支持 Range 并嵌入限速
前端视频拖拽、断点续传依赖 Range 请求,而标准 http.ServeContent 不自带限速,需手动注入。
- 不要裸用
io.Copy(w, file):它绕过Range解析,且无速率控制 - 先用
http.ServeContent处理Range逻辑,拿到要返回的偏移与长度后,再包装一个限速io.Reader - 示例:构造
limitedReader := &limitedReader{r: file, limiter: readLimiter, n: contentLength},每次Read(p)前调用limiter.WaitN(ctx, len(p)) - 缓冲区建议显式设为
make([]byte, 64*1024),避免默认 32KB 在高并发下分配过多小对象
大文件上传流式解析 + 分片写入时如何限速
上传路径中限速点有三处:HTTP body 读取、multipart part 解析、磁盘写入。任一环节失控都会放大内存压力。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
立即学习“go语言免费学习笔记(深入)”;
- 用
r.MultipartReader()替代r.ParseMultipartForm(),避免内存堆积;NextPart()返回的part.Body是io.Reader,可直接套限速 wrapper - 对每个 part 的写入限速:打开
os.File后,用bufio.NewWriterSize(f, 1)包装,并在Write前等配额;不要等整个 part 写完才 flush,否则缓冲区可能撑爆 - 前端分片上传时,服务端应对每个 chunk 单独限速(如每 chunk ≤5MB,限速 2MB/s),而非全局限速——避免小 chunk 等待过大配额
- 务必在 handler 中用
http.MaxBytesReader封顶总上传量,防止恶意请求绕过分片逻辑耗尽资源
容易被忽略的粘包与限速冲突
限速本身不导致粘包,但会让协议层更脆弱——尤其对定长头、长度前缀类协议。
- 例如
io.ReadFull(conn, header[:4])本该一次读完 4 字节头,但限速太低可能导致返回 2+2 拆包,后续解析失败 - 解决办法:限速 wrapper 必须放在协议解析之后,即先用未限速连接读出完整 header,再对 payload 部分限速转发
- 若用
bufio.Scanner或bufio.Reader.ReadString,不要在其上叠加限速——这些高层方法内部已做缓冲,限速点错位会导致行为不可预测 - 高并发下,多个限速器共用同一
rate.Limiter实例会串行等待,应为每个连接新建独立实例,或用 per-connection token bucket
真正难的不是加限速,而是让限速不破坏协议语义、不放大超时风险、不引入隐式内存引用。多数线上问题都出在 deadline 与 limiter 不匹配,或把限速插在了协议解析之前。

















