登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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

来源:17golang原创

时间:2026-07-22 15:14:46 395浏览 收藏

文章详情接口每次返回相同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删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>