当前位置:首页 > 文章列表 > Golang > Go教程 > Go net/http 如何正确返回 ETag:If-None-Match 与 304 缓存协商

Go net/http 如何正确返回 ETag:If-None-Match 与 304 缓存协商

来源:17golang原创 2026-07-22 15:14:46 0浏览 收藏

文章详情接口每次返回相同JSON内容时,浏览器没法判断内容有没有改动,只能完整拉取整份响应体。给 Go 的 net/http 响应加上 ETag 后,浏览器下次发起同地址请求时会自动带上 If-None-Match;如果服务端判断资源版本没有变化,直接返回 304 Not Modified,不需要再重复传输响应正文。

ETag 的核心不是随便算个哈希值就完事,而是先生成和响应内容严格绑定的稳定资源标识,再在写入响应体之前判断 If-None-Match,命中条件时只返回 304 状态码和缓存相关响应头就可以。

要点速览

  • ETag 必须由同一份响应内容稳定生成,生成过程中要注意JSON字段顺序不能随意变动。
  • 命中 If-None-Match 条件时,直接把状态码改为304,同时保留 ETag、Cache-Control 等必要响应头。
  • 304 响应不能附带任何响应正文,条件判断逻辑必须放在 WriteHeader 或实际写入 body 操作之前执行。
  • 功能测试必须覆盖首次请求返回200、命中规则返回304、内容更新后重新返回200三条核心路径。

ETag 协商到底省下了什么

假设接口 /api/articles/17 返回一篇大小18KB的JSON格式文章。第一次访问时,服务端返回200状态码和完整正文,同时附带一个ETag头:

HTTP/1.1 200 OK
ETag: "article-17-v3"
Cache-Control: private, no-cache
Content-Type: application/json

{"id":17,"title":"...","body":"..."}

浏览器会把这个ETag标识缓存在本地。再次访问同一个接口时,请求头里就会自动带上对应标识:

GET /api/articles/17 HTTP/1.1
If-None-Match: "article-17-v3"

如果文章内容还是v3版本没有改动,服务端只需要返回响应头即可:

HTTP/1.1 304 Not Modified
ETag: "article-17-v3"

这里节省的带宽来自“省去了重复传输正文”的部分,并没有省略这次HTTP请求本身。当文章内容更新后,ETag标识也会同步变化,客户端才会重新拉取带完整正文的200响应。

ETag 缓存协商资源预算图:首次请求返回 200 正文,命中 If-None-Match 后返回 304 省掉响应体

用稳定内容生成强 ETag

先把接口最终要返回的内容编码成字节数组,再对原始字节计算SHA-256得到标识。不要直接对map结构格式化后就把结果当作版本号:字段顺序、多余空格、时间格式的微小变动,都可能导致内容没有变化却生成了完全不同的新标识。

package main

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

type Article struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
    Body  string `json:"body"`
}

func strongETag(body []byte) string {
    sum := sha256.Sum256(body)
    return `"` + hex.EncodeToString(sum[:]) + `"`
}

func articleHandler(w http.ResponseWriter, r *http.Request) {
    article := Article{ID: 17, Title: "Go 缓存协商", Body: "把响应版本交给内容决定。"}
    body, err := json.Marshal(article)
    if err != nil {
        http.Error(w, "编码文章失败", http.StatusInternalServerError)
        return
    }

    etag := strongETag(body)
    w.Header().Set("ETag", etag)
    w.Header().Set("Cache-Control", "private, no-cache")
    w.Header().Set("Content-Type", "application/json; charset=utf-8")

    if r.Header.Get("If-None-Match") == etag {
        w.WriteHeader(http.StatusNotModified)
        return
    }

    w.WriteHeader(http.StatusOK)
    _, _ = w.Write(body)
}

func main() {
    http.HandleFunc("/api/articles/17", articleHandler)
    port := 8080
    _ = http.ListenAndServe(":"+strconv.Itoa(port), nil)
}

这里生成的标识带双引号,符合通用的强ETag格式规范。响应头要在设置状态码之前完成写入,一旦响应流已经开始向外输出,后续再修改ETag头就不会生效了。

先写 ETag,再决定 200 还是 304

条件判断的执行顺序可以简化成三步:编码生成响应正文、写入缓存相关响应头、比对客户端带来的条件请求头。如果第三步命中304规则,要立刻终止当前处理函数,不能继续往下写入JSON内容。

  • 首次请求没有 If-None-Match 头:直接返回200状态码、ETag头和完整JSON正文。
  • 请求头携带的标识和当前生成的ETag完全一致:返回304状态码,仅输出必要的响应头。
  • 文章内容发生过改动:生成新的ETag标识,返回200状态码和更新后的新正文。

如果业务代码里把“统一返回JSON”的中间件放在缓存协商逻辑之后执行,命中304规则后很可能还会被中间件追加多余的错误正文。建议把缓存协商逻辑尽量放在资源处理器附近,或者让统一响应层识别到304状态码后直接停止任何写入正文的操作。

Go net/http ETag 决策路径:生成内容标签后分流到 304 命中或 200 返回正文

用 curl 复查三条请求路径

启动服务之后,先发起第一次请求,拿到响应里的ETag值:

curl -i http://127.0.0.1:8080/api/articles/17

假设终端输出拿到的标签是 "a1b2...",再把这个标签加到请求头里重新发起请求:

curl -i \
  -H 'If-None-Match: "a1b2..."' \
  http://127.0.0.1:8080/api/articles/17

预期返回状态码是304,且响应里没有携带任何JSON正文。最后修改 Title 或者 Body 的内容再发起请求,状态码应该变回200,ETag的值也会同步更新。

测试的时候不要只检查状态码是否正确,要用 -i 参数同时校验ETag、Cache-Control、Content-Type这些响应头;如果304响应还附带了多余的正文,通常是某个中间件或者封装层在处理器返回后继续写入内容导致的。

几个容易踩到的缓存边界

动态用户数据不要共用公共缓存

如果文章详情接口里混入了用户点赞数、登录态、权限校验这类个性化字段,就不能直接套用公共缓存策略。示例使用 private 配置,表示缓存内容只属于当前用户代理;如果要将缓存范围扩大到CDN层,必须先拆分出公共内容部分和个性化字段部分分开处理。

弱校验不能当成强校验使用

W/"..." 是弱ETag的标识,适合语义等价但字节表现可能存在差异的资源。本文场景下JSON直接由稳定字节计算生成,因此使用强ETag即可;不要给每次响应都生成随机字符串,那样永远不可能命中304缓存逻辑。

压缩响应要绑定实际表示

如果gzip、br这类内容压缩操作由服务端动态选择,ETag的生成策略要和实际返回的内容表示完全对齐,或者交给网关层统一处理 Vary: Accept-Encoding 逻辑。不然同一个ETag标识可能被错误绑定到不同编码格式的响应上,引发缓存异常。

常见问题

304 是否需要返回 JSON 正文?

不需要。客户端会直接使用本地已经缓存的旧正文;服务端只需要保留必要的响应头,在写入正文之前结束整个处理流程即可。

ETag 一定要用 SHA-256 吗?

不一定,核心要求是生成逻辑稳定、碰撞风险可控且生成开销合适。如果响应内容体积很大,也可以直接使用数据库里存的版本号或者更新时间加文件长度组合生成,只要能准确标识资源的变化状态就可以。

为什么每次都返回 200?

先确认客户端是否真的发送了 If-None-Match 请求头,再检查请求头里的标识是否包含正确的双引号、当前生成的ETag是否稳定,以及条件判断逻辑有没有放在写入响应操作之后执行。

Cache-Control 和 ETag 能单独使用吗?

可以,但二者职责不一样。Cache-Control 用来规定缓存能不能存、隔多久要回源校验;ETag 是回源校验阶段用来判断资源有没有实际变动的标识,两个搭配使用更容易精准控制缓存行为。

小结

Go 里实现 ETag 逻辑本身并不复杂,真正容易出错的是响应输出的时机判断和资源边界的处理:先把最终要返回的正文编码成稳定字节,设置好ETag和缓存策略,再比对 If-None-Match 请求头。命中条件就直接返回304,没有命中再写入200响应的正文。用curl把首次访问、命中缓存、内容更新这三条路径完整跑通,缓存协商逻辑才算真正落地。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go atomic.Bool 怎么做运行时功能开关:并发读取、灰度切换与回滚Go atomic.Bool 怎么做运行时功能开关:并发读取、灰度切换与回滚
上一篇
Go atomic.Bool 怎么做运行时功能开关:并发读取、灰度切换与回滚
Go bufio.Scanner 遇到 token too long 怎么办:大日志行的长度上限与内存取舍
下一篇
Go bufio.Scanner 遇到 token too long 怎么办:大日志行的长度上限与内存取舍
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    173次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    106次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    34次使用
  • LangGPT提示词框架:结构化Prompt设计方法与开源工具指南
    LangGPT
    LangGPT是一种受编程语言启发的结构化提示词设计工具,提供双层框架、模块化模板及变量功能,帮助用户高效编写高质量Prompt。该项目已在GitHub免费开源,适用于内容创作、编程辅助等多场景。
    42次使用
  • ClickPrompt:AI提示词生成与优化工具,支持Stable Diffusion、ChatGPT及代码辅助
    ClickPrompt
    ClickPrompt是一款专为AI提示词编写者设计的开源在线工具,支持Stable Diffusion绘图、ChatGPT对话及GitHub Copilot代码辅助。提供Prompt自动生成、一键运行、社区分享及可视化优化功能,帮助用户高效获取精准AI输出。
    79次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码