当前位置:首页 > 文章列表 > Golang > Go问答 > Go ETag 条件请求为什么总是返回 200

Go ETag 条件请求为什么总是返回 200

来源:17golang原创 2026-11-03 02:29:59 0浏览 收藏

Go 服务已经给响应加了 ETag,客户端下一次带着 If-None-Match 回来,却仍然得到 200,最常见的原因是服务端没有把这两个值放进同一个条件判断。ETag 不是“加上响应头就自动生效”的开关:handler 需要先生成当前资源标签,再读取请求头,匹配成功时在写响应体之前返回 304 Not Modified。

本文只处理 net/http 中的 ETag 条件 GET。先记住排障结论:请求头缺失、标签格式不同、资源已经变化、handler 太早写出 200,以及代理层重新生成响应,都可能让你看到 200。官方文档入口如下,便于对照 Request、Header 和 ResponseWriter 的 API。

官方文档:https://pkg.go.dev/net/http

ETag 匹配时应该在写入响应体之前返回 304;只有标签不匹配或请求不是条件 GET 时,才发送资源正文和 200。返回 200 本身不一定是 Go 出错,关键是先确认服务端收到的 If-None-Match 和当前 ETag 是否代表同一个版本。

先确认条件请求到底带了什么

排查不要先盯着浏览器的“是否命中缓存”。先把一次请求拆成四个事实:方法和路径、客户端发来的 If-None-Match、handler 计算出的当前 ETag、最终写出的状态码。如果第二项为空,服务端自然只能走 200;如果两项看起来一样却仍为 200,就继续查比较逻辑和响应写入时机。

ETag 的值通常是带引号的实体标签,例如 "article-v17",弱标签则是 W/"article-v17"。不要把它当作普通业务 ID 丢掉引号,也不要在一个请求中重新随机生成标签,否则客户端永远拿不到稳定的版本标识。

按 HTTP 语义比较 ETag

最小实现可以先处理单个标签,但生产代码至少要考虑逗号分隔的多个标签和 *。下面的辅助函数只负责“当前资源是否被 If-None-Match 覆盖”,不负责写响应,因此更容易单独检查。

package etagdemo

import "strings"

// ifNoneMatchContains 判断请求头是否包含当前实体标签。
// 这里只去掉弱标签前缀用于缓存命中判断,不改变响应中发送的原始 ETag。
func ifNoneMatchContains(headerValue, currentETag string) bool {
	if strings.TrimSpace(headerValue) == "*" {
		return true // 星号表示已有任意当前表示。
	}

	current := strings.TrimSpace(currentETag)
	if strings.HasPrefix(current, "W/") {
		current = strings.TrimSpace(strings.TrimPrefix(current, "W/"))
	}
	for _, candidate := range strings.Split(headerValue, ",") {
		value := strings.TrimSpace(candidate)
		if strings.HasPrefix(value, "W/") {
			value = strings.TrimSpace(strings.TrimPrefix(value, "W/"))
		}
		if value == current {
			return true
		}
	}
	return false
}

这段代码的边界要说清楚:它适合 GET/HEAD 的缓存命中判断,不能替代需要强校验的并发写入条件。比如更新接口通常还要严格区分弱标签,并结合 If-Match 的强比较语义。标题讨论的是“为什么总是 200”,重点是先让读取路径的判断闭环。

If-None-Match 与当前 ETag 比较后返回 304 或 200 的静态结构图
图1:ETag 条件请求的输入、比较和响应状态关系(静态说明图,不是运行截图)。

在写入响应体前返回 304

很多“总是 200”的实现已经设置了 ETag,却先调用了 WriteHeader(200)、Write 或 http.ServeContent,之后才判断请求头。HTTP 响应状态一旦写出,再改成 304 就晚了。正确顺序是:读取资源版本,设置当前 ETag,比较请求头,命中后设置缓存相关头并直接返回。

package main

import (
	"crypto/sha256"
	"encoding/hex"
	"net/http"
)

var articleBody = []byte("ETag protects a stable representation")

// makeETag 用资源内容生成稳定标签;内容没变时,跨请求标签也不变。
func makeETag(body []byte) string {
	sum := sha256.Sum256(body)
	return `"` + hex.EncodeToString(sum[:8]) + `"`
}

// articleHandler 先完成条件判断,再决定返回 304 还是带正文的 200。
func articleHandler(w http.ResponseWriter, r *http.Request) {
	currentETag := makeETag(articleBody)
	w.Header().Set("ETag", currentETag)
	w.Header().Set("Cache-Control", "private, max-age=60")

	if r.Method == http.MethodGet || r.Method == http.MethodHead {
		if ifNoneMatchContains(r.Header.Get("If-None-Match"), currentETag) {
			w.WriteHeader(http.StatusNotModified) // 304 不应写资源正文。
			return
		}
	}

	if r.Method == http.MethodHead {
		w.WriteHeader(http.StatusOK) // HEAD 只给响应头,不写正文。
		return
	}
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	w.WriteHeader(http.StatusOK) // 未命中条件标签时才发送 200。
	_, _ = w.Write(articleBody)
}

func main() {
	http.HandleFunc("/article", articleHandler)
	_ = http.ListenAndServe(":8080", nil) // 示例服务;生产环境应配置超时和优雅退出。
}

实现中有三个容易忽略的点。第一,ETag 在 200 和 304 两条路径都要存在,方便客户端继续使用当前版本。第二,304 响应不应携带资源正文。第三,HEAD 可以复用同一套标签判断,但它本来就不发送正文,不能把 HEAD 的行为误当成 GET 缓存命中。

定位为什么仍然返回 200

如果请求已经携带 If-None-Match,可以按下面顺序定位。每一步只回答一个问题,不要同时修改缓存头、代理配置和 handler,否则很难知道是哪一层起作用。

  1. 请求是否真的到达了这个 handler:确认路径、方法和路由没有被重定向或转发到另一套处理函数。
  2. 请求头是否被保留:在服务端日志中记录是否存在和长度即可,不要记录用户凭据;反向代理可能会删除或重写条件头。
  3. 当前 ETag 是否稳定:如果标签带时间戳、随机数或每次序列化顺序都变化,比较必然失败。
  4. 比较是否发生在第一次写出之前:一旦调用 Write,Go 通常会隐式写出 200,后续再调用 WriteHeader(304) 不会得到想要的结果。
  5. 客户端看到的状态是否来自源站:CDN、网关或缓存服务可能替源站生成响应,观察源站日志和代理的响应头要分开。
Go ETag 条件请求返回 200 时的四组排障信号矩阵
图2:返回 200 时优先对照的四组排障信号(静态结构图,不是运行证据)。

用表格区分几种 200

现象更可能的原因处理动作
If-None-Match 为空客户端尚未缓存,或代理删除请求头先检查响应是否确实发出 ETag,再检查转发规则
请求标签与当前标签不同资源版本已经变化,或标签生成不稳定记录两者的规范化值,确认是否绑定同一份表示
两者相同但响应为 200比较分支未执行,或正文已提前写出把比较移动到所有 Write/WriteHeader 之前
源站日志为 304,客户端为 200中间层重新请求或改写了响应分别查看源站和代理的请求 ID、状态和缓存头

注意,调试日志不要把完整的 Cookie、Authorization 或正文打印出来。ETag 通常不是秘密,但把它和用户标识、完整请求头绑定输出,仍可能让排障日志失去最小化原则。

并发更新和缓存头的边界

ETag 只描述某一份表示的版本,不会自动解决资源更新时的并发冲突。如果文章正文来自数据库或对象存储,应在读取同一版本数据时计算标签,避免正文来自版本 A、ETag 却来自版本 B。更新接口的乐观锁则应单独设计,不要因为 GET 返回过 304 就把弱标签当成写入授权。

Cache-Control 也不能省略。ETag 负责“这份表示是否仍然匹配”,缓存策略负责“客户端和中间层什么时候可以复用它”。如果响应需要用户隔离,使用合适的 private 或其他明确策略;不要为了看到 304 而把个性化正文标成公共缓存。

// cacheHeaders 给同一资源版本设置一致的缓存元数据。
func cacheHeaders(w http.ResponseWriter, etag string) {
	w.Header().Set("ETag", etag)
	w.Header().Set("Cache-Control", "private, max-age=60")
	// 资源内容会随语言变化时,必须把语言维度纳入缓存键。
	w.Header().Add("Vary", "Accept-Language")
}

常见问题

ETag 一定要用 SHA-256 吗?

不一定。它只需要稳定地标识某次表示,使用内容摘要、数据库版本号或对象存储版本号都可以。摘要要避免过度截断造成碰撞,版本号则必须和实际响应内容同步。

请求带了 If-None-Match,服务端能直接返回 304 吗?

不能只看请求头是否存在,还要比较它是否匹配当前表示。没有匹配时仍应返回完整资源,通常是 200。

为什么设置 ETag 后浏览器还是下载正文?

先确认第二次请求真的带上了 If-None-Match,再查标签是否每次变化、响应是否提前写出以及中间层是否替换了状态。只设置响应头不会替 handler 完成条件判断。

304 响应里能返回 JSON 提示吗?

不应把 304 当作业务 JSON 响应。客户端应复用自己已有的缓存表示;如果需要返回新的业务数据,就应发送对应的 200 或其他明确状态。

小结

Go ETag 条件请求总是返回 200,排查核心不是反复刷新客户端,而是核对“请求标签—当前标签—比较时机—最终写出层”这条链路。把 ETag 绑定到稳定资源版本,使用兼容的 If-None-Match 判断,并在写正文前结束 304 分支,通常就能解决服务端实现造成的 200。若源站已经返回 304,再把注意力转向代理和缓存层。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go HTTP Range 返回错误长度通常是哪类问题Go HTTP Range 返回错误长度通常是哪类问题
上一篇
Go HTTP Range 返回错误长度通常是哪类问题
Go 代理转发请求为什么 Host 变了
下一篇
Go 代理转发请求为什么 Host 变了
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    850次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    855次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    936次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    910次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    739次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码