Go中HTTP响应头必须在Write()或WriteHeader()前设置,否则被忽略;Set()覆盖同名头,Add()追加;框架/中间件可能覆盖Content-Type;动态类型应使用mime.FormatMediaType安全构造;CORS需单独处理OPTIONS预检请求。

用 ResponseWriter.Header().Set() 直接写头
Go 的 HTTP 响应头必须在 Write() 或 WriteHeader() 调用前设置,否则会被忽略——这是最常踩的坑。因为 ResponseWriter 内部有“已写入”状态,一旦开始写响应体(哪怕只写了一个字节),头就锁死了。
正确做法是:先调用 Header().Set() 或 Header().Add(),再调用 Write() 或 WriteHeader():
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok": true}`))
}
-
Set()会覆盖同名头(适合单值头,如Content-Type) -
Add()会追加同名头(适合多值头,如Set-Cookie) - 如果没显式调用
WriteHeader(),Write()会自动发200 OK,但此时头仍可设——只要还没真正写出响应体
Content-Type 不生效?检查是否被框架/中间件覆盖
很多 Go Web 框架(如 Gin、Echo)或中间件(如日志、CORS)会在你之后再次调用 Header().Set(),把你设的 Content-Type 给冲掉。典型表现是:自己写了 w.Header().Set("Content-Type", "text/plain"),但浏览器收到的却是 application/json。
- Gin 默认在
c.JSON()里设Content-Type: application/json,且不给你留修改机会——想改就得用c.Data()手动写 - Echo 的
c.String()会硬编码text/plain,无法绕过;得用c.Blob()+ 手动设头 - 自定义中间件里调用了
next(c)后又操作了w.Header(),也可能覆盖你前面的设置
需要动态 Content-Type?别拼字符串,用标准 MIME 包
手动拼 "application/json; charset=utf-8" 看似简单,但容易漏空格、错大小写,或在不同字符集下出问题。Go 标准库的 mime 包提供了安全构造方式。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
例如生成带 charset 的 JSON 类型:
import "mime"
ctype := mime.FormatMediaType("application/json", map[string]string{"charset": "utf-8"})
w.Header().Set("Content-Type", ctype)
-
mime.FormatMediaType()自动处理分号、引号、转义,比手拼更可靠 - 对
text/html、text/css等文本类型,务必显式指定charset,否则浏览器可能按 ISO-8859-1 解析中文 - 二进制类型(如
image/png)不需要charset,加了反而可能被某些客户端拒绝
跨域响应头(CORS)为什么有时不生效?顺序和预检有关
如果你加了 Access-Control-Allow-Origin 但前端仍报 CORS 错误,大概率不是头没设,而是触发了预检(preflight)请求——浏览器对非简单请求(如带自定义头、PUT 方法、application/json 体)会先发一个 OPTIONS 请求,而你的 handler 可能根本没处理它。
- 必须为
OPTIONS请求单独返回 204 + CORS 头,不能只靠主 handler 设置 -
Access-Control-Allow-Headers必须精确匹配前端实际发送的自定义头名(比如前端发X-User-ID,这里就必须写这个字符串,不能写*—— 除非没带凭据) -
Access-Control-Allow-Credentials: true时,Access-Control-Allow-Origin不能为*,必须是具体域名
头设置本身很简单,难的是理清浏览器什么时候走预检、什么时候直接发主请求,以及对应 handler 是否覆盖了所有路径和方法。

















