Go 的 http.Header 实际上不区分大小写,所有键操作均按 canonicalMIMEHeaderKey 规则归一化为首字母大写、其余小写的形式(如 "Content-Type"),符合 RFC 7230 规范。

Go 的 http.Header 实际上不区分大小写
Go 标准库的 http.Header 类型在底层用 map[string][]string 存储,但所有键操作(Get、Set、Add、Del)都自动做了 ASCII 大小写归一化:把键转成首字母大写、其余小写的形式(即 canonicalMIMEHeaderKey 规则)。这意味着 h.Get("content-type") 和 h.Get("Content-Type") 返回完全一样的值。
这不是 bug,是 Go 明确设计的行为,符合 HTTP/1.1 RFC 7230 —— header 字段名本就应被视作不区分大小写。
-
http.Header的map键存储的是规范化的形式(如"Content-Type"),不是你原始传入的字符串 - 如果你用
range遍历h,看到的 key 是规范化后的,不是原始拼写 - 调用
h.Set("cOnTeNt-TyPe", "application/json"),内部存的 key 是"Content-Type"
读取客户端请求时 Header 大小写“丢失”是正常现象
当你在 http.HandlerFunc 中拿到 *http.Request,它的 r.Header 已经完成标准化。你无法通过 r.Header 还原客户端发来的原始 header 名大小写(比如 "CONTENT-TYPE" 或 "content-type")。
这是因为 net/http 在解析请求时就做了归一化,没保留原始字节序列 —— 它只关心语义,不关心拼写。
立即学习“go语言免费学习笔记(深入)”;
- 不要试图从
r.Header反推客户端行为,它不可靠 - 如果真需要原始 header 行(例如做协议调试或合规审计),得自己实现
http.ReadRequest或用http.Transport的RoundTrip拦截原始字节流,代价高且非常规 - 绝大多数业务场景下,这种“丢失”毫无影响:header 值本身才是关键
手动构造响应 Header 时大小写无关紧要,但注意 WriteHeader 时机
你可以用任意大小写调用 w.Header().Set("X-My-Header", "foo"),最终写出的响应里 header 名一定是 "X-My-Header"(规范化后)。但要注意:一旦调用过 w.WriteHeader() 或开始写 body,再改 w.Header() 就无效了。
-
w.Header().Set("x-my-header", "bar")和w.Header().Set("X-My-Header", "bar")效果完全一样 - 如果先调用了
w.WriteHeader(200),后续对w.Header()的修改会被忽略 —— 这是常见坑点,尤其在中间件里误判状态码写入时机 - 想确保 header 生效,必须在第一次
w.Write()或w.WriteHeader()之前完成设置
第三方库或自定义 parser 出现大小写问题?先确认是不是绕过了标准 http.Header
有些场景会跳过标准库的 header 解析逻辑,比如直接读 bufio.Reader、用 fasthttp、或解析 multipart boundary 时手撕 header 行。这时大小写行为由你代码决定,不再受 Go 规范约束。
典型表现:h.Get("Authorization") 返回空,但你知道客户端确实发了 "authorization: Bearer ..." —— 很可能你用了非标准 parser,或者误把 raw bytes 当成了已归一化的 http.Header。
- 检查是否误用了
req.Header.Clone()后又手动改 map(Clone()返回的是新 map,但 key 仍是规范化形式) - 用
fasthttp时,它的Request.Header.Peek("authorization")是严格大小写敏感的,和标准库行为不同 - 调试时可打印
fmt.Printf("%#v", r.Header)看实际 key 列表,比猜更可靠
真正容易被忽略的点是:header 归一化发生在解析层,而不是传输层。你在 Wireshark 里看到的原始大小写,和 Go 程序里能访问到的,根本不在同一个抽象层级。别试图让应用逻辑依赖“客户端怎么写的 header 名”,那只会把简单问题搞复杂。










