
本文详解如何用 go 编写一个可嵌入 github readme 的动态 svg 徽章服务,并解决 github 代理层强缓存导致状态不更新的核心问题。关键在于正确设置 http 缓存控制头(如 `cache-control: no-cache`、`etag` 和 `last-modified`),绕过 github 的 cdn 缓存。
GitHub README 中的图片由 GitHub 的反向代理(基于 Fastly)缓存,即使你的 Go 服务返回 Cache-Control: no-cache,GitHub 仍可能忽略或覆盖该头,导致徽章“卡死”在旧状态。实测表明:仅靠 no-cache 不足以强制刷新;必须配合强校验机制(如 ETag + If-None-Match 协商)和语义化响应头,才能让 GitHub 每次请求都回源获取最新状态。
以下是一个生产就绪的 Go 徽章服务实现,已通过 GitHub README 实时切换验证(如每刷新一次页面即切换 “correct”/“wrong”):
package main
import (
"crypto/md5"
"fmt"
"log"
"net/http"
"time"
)
var (
badges = map[string][]byte{
"correct": []byte(``),
"wrong": []byte(``),
}
state = false
lastMod = time.Now().UTC().Truncate(time.Second) // 确保 Last-Modified 可预测
)
func badgeHandler(w http.ResponseWriter, r *http.Request) {
// ✅ 强制禁用缓存(GitHub 明确识别)
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
// ✅ 提供 Last-Modified 和 ETag(基于当前状态生成唯一指纹)
etag := fmt.Sprintf(`"%x"`, md5.Sum([]byte(fmt.Sprintf("%t-%s", state, lastMod.Unix()))))
w.Header().Set("ETag", etag)
w.Header().Set("Last-Modified", lastMod.Format(http.TimeFormat))
// ✅ 支持条件请求:若客户端(GitHub)携带匹配 ETag,则返回 304
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
// ✅ 切换状态(真实场景中建议用原子操作或 channel 控制)
state = !state
key := "correct"
if !state {
key = "wrong"
}
// ✅ 设置内容类型与长度(避免 Transfer-Encoding: chunked 导致解析失败)
w.Header().Set("Content-Type", "image/svg+xml")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(badges[key])))
// ✅ 写入 SVG 内容
w.Write(badges[key])
}
func main() {
http.HandleFunc("/view", badgeHandler)
log.Println("Badge server listening on :8085")
log.Fatal(http.ListenAndServe(":8085", nil))
}关键要点说明:
- ETag 必须随状态变化而变化:我们使用 md5(state + timestamp) 生成强 ETag,确保每次状态翻转都产生新指纹,触发 GitHub 回源。
- Last-Modified 需为标准格式且稳定:GitHub 会校验其合法性,使用 time.UTC().Truncate(time.Second) 避免毫秒级差异引发协商失败。
- 显式设置 Content-Length:防止 Go 默认启用分块传输(chunked encoding),某些 GitHub 代理对无长度的 SVG 响应处理异常。
- Timing-Allow-Origin: https://github.com(可选增强):若需前端调试,可添加此头支持跨域 Timing API,但非必需。
在 README 中正确嵌入:

⚠️ 注意:不要加括号空链接语法 []() —— 这会导致 GitHub 渲染为纯文本而非图片。务必使用标准 Markdown 图片语法。
验证是否生效:
- 启动服务后,在浏览器中多次访问 /view,观察 SVG 是否交替变化;
- 将链接粘贴至 GitHub README 并提交;
- 硬刷新(Ctrl+Shift+R)README 页面,检查徽章是否实时响应(非首次加载缓存);
- 使用 curl -I http://your-domain.com/view 检查响应头是否包含 ETag、Last-Modified 和 Cache-Control: no-cache。
? 提示:若仍被缓存,可临时在 URL 后添加时间戳参数(如 /view?t=123)作为兜底方案,但推荐优先修复 ETag 逻辑——它更规范、零带宽开销,且符合 GitHub 官方 badge 服务(如 Travis CI)的实践模式。










