必须分段设限并配合 context 主动检查超时:ParseMultipartForm 会持续读取整个请求体,需用 context.WithTimeout 包裹,替换请求上下文后再调用。

Go 处理大文件上传时,只设一个 http.Client.Timeout 或全局 http.Server.ReadTimeout 是无效的——它既拦不住恶意 multipart 流,也救不了卡在磁盘 I/O 的 goroutine。必须按链路分段设限,且每段都要配合 context 主动检查。
ParseMultipartForm 的超时必须用 context.WithTimeout 包裹
r.ParseMultipartForm 看似只是解析表单,但它会持续读取整个请求体:小文件进内存,大文件落临时磁盘,而这个过程完全不响应网络超时。攻击者发一个 1GB 的伪造 multipart 流,它就会一直读直到 OOM 或连接断开。
- 必须在调用前创建带时限的 context:
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) - 用
r = r.WithContext(ctx)替换原请求上下文,再调用r.ParseMultipartForm(32 - 错误判断要区分:
errors.Is(err, context.DeadlineExceeded)才是真超时,其他如multipart.ErrMessageTooLarge属于解析失败 - 别忘了
defer cancel(),否则 context 泄漏
multipart.File 读取必须封装为 ctx-aware reader
multipart.File 是 io.ReadCloser,但它的 Read() 方法不接受 context.Context。直接丢给 io.Copy(dst, file) 就等于放弃超时控制——哪怕 context 已取消,读操作仍会卡死在系统调用上。
- 不要用原始
file,而是包装:&ctxReader{Reader: file, ctx: ctx} -
ctxReader.Read内部用select { case 主动退出 - 如果用了 Go 1.19+,可改用
io.CopyContext(dst, file),它内部已做同样逻辑 - 务必在读完后调用
file.Close(),否则临时文件句柄不释放
客户端上传超时需 Transport 层分级控制
服务端 handler 超时管不了客户端发包慢的问题。比如用户上传 500MB 文件,但上行带宽只有 1MB/s,光发送就得 500 秒——这时需要的是写超时(WriteTimeout),不是连接或响应超时。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
-
DialContext控建连:建议5 * time.Second,防 DNS 卡顿或不可达主机 -
WriteTimeout控请求体发送完成:按最大文件大小和最低带宽估算,例如 100MB / 1MB/s → 至少120 * time.Second -
ReadTimeout控响应读取:通常30 * time.Second足够,业务逻辑不应在此阶段耗太久 - 别漏掉
IdleConnTimeout,避免连接池积压大量空闲连接
http.MaxBytesReader 必须放在 handler 最开头
很多人把 http.MaxBytesReader 放在 ParseMultipartForm 后面,这是错的。multipart 解析器在读 boundary、header 时就会分配缓冲区,恶意构造的超长 boundary 可以绕过所有后续校验,直接打爆内存。
- 第一行就该写:
r.Body = http.MaxBytesReader(w, r.Body, 2*1024*1024*1024)(限制 2GB) - 这个 wrapper 会在每次
Read()时累计字节数,超限立即返回http.ErrBodyReadAfterClose - 错误响应要用
http.Error(w, "", http.StatusRequestEntityTooLarge),别继续往下走 - 如果用了 Gin 等框架,必须在中间件里替换
c.Request.Body,不能等进 handler 才处理
最容易被忽略的是:os.File 的 Read 和 Write 本身不响应 context,即使你传了带 cancel 的 context,底层系统调用仍会阻塞。所以所有文件 IO 都得靠 io.CopyContext 或手动分块 + select 监听 ctx.Done() —— 没有捷径,也不能省。

















