Go HTTP服务默认不发CORS头,必须手写中间件:对所有请求设Access-Control-Allow-Origin等头,OPTIONS请求立即返回204,带credentials时Origin不能为*而须指定域名。

Go HTTP 服务默认不发任何 CORS 响应头,浏览器看到缺失 Access-Control-Allow-Origin 就直接拦截请求——不是后端没收到,是根本没发出去。必须手动加逻辑,且 OPTIONS 预检必须显式处理。
怎么写一个能用的 CORS 中间件(net/http)
手写中间件最轻量,适合简单场景,但三个点不能错:
- 所有请求(包括非 OPTIONS)都要设
Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers -
r.Method == "OPTIONS"必须立即返回http.StatusNoContent(204),且不能调用next.ServeHTTP() - 如果前端用了
fetch({ credentials: 'include' }),Access-Control-Allow-Origin不能是"*",得写死具体域名,比如"https://myapp.com",同时必须加Access-Control-Allow-Credentials: "true"
示例片段:
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Requested-With")
w.Header().Set("Access-Control-Expose-Headers", "X-Total-Count")
}
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}为什么 gin-contrib/cors 比手写更稳
手写中间件容易在 AllowCredentials 和 AllowOrigins 配置上翻车:只要 AllowCredentials: true 且 AllowOrigins 含 "*",浏览器就静默丢弃响应,Network 面板里都看不到完整失败原因。
立即学习“go语言免费学习笔记(深入)”;
-
gin-contrib/cors自动校验二者互斥关系,AllowOrigins设为"*"时会强制禁用AllowCredentials - 它自动拦截并响应 OPTIONS 请求,不触发后续 handler(避免数据库误写等副作用)
- 支持
MaxAge缓存预检结果,减少重复 OPTIONS 往返 - 必须在
r.Use()中注册,且要在任何r.GET()等路由定义之前,否则中间件不生效
配置示例:
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://myapp.com", "http://localhost:3000"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
ExposeHeaders: []string{"X-Total-Count"},
MaxAge: 12 * 60 * 60,
}))gorilla/handlers.CORS 为什么常被选作生产方案
它比 gin-contrib/cors 更通用,适配 net/http、echo、fiber 等任意基于 http.Handler 的框架,且对 header 设置时机、OPTIONS 响应体、缓存控制做了更细粒度封装。
- 若用
handlers.AllowedOrigins([]string{"*"}),它内部会自动跳过AllowCredentials,避免浏览器报错 -
handlers.ExposedHeaders([]string{"X-Request-ID"})是显式白名单,没列的 header 前端 JS 拿不到 - 注意:Nginx 后面跑 Go 服务时,必须配置
add_header Access-Control-Allow-Origin $sent_http_access_control_allow_origin always;,否则 Nginx 会清空 Go 写的 header
跨域失败时先盯住这三个地方
90% 的问题出在这三处,别一上来就改代码:
- 浏览器 Network 面板里看 OPTIONS 请求是否返回 204/200;如果返回 404 或 405,说明路由没覆盖到 OPTIONS,中间件没注册对位置
- 检查响应头里有没有
Access-Control-Allow-Origin,如果有但值是"*",而前端又带了 cookie,那这个响应会被浏览器直接丢弃 - 用 curl 模拟预检:
curl -I -X OPTIONS -H "Origin: https://myapp.com" -H "Access-Control-Request-Method: POST" http://localhost:8080/api/user,看返回头是否齐全
真正难的不是加几行 header,而是理解浏览器预检机制和凭证模式之间的硬性约束——这些规则由浏览器强制执行,Go 本身无权绕过。


















