本文详解如何用 go 编写一个可嵌入
git
hub 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”):
Comet AI 浏览器
下载 Comet AI 浏览器,体验由 Perplexity AI 驱动的革命性上网方式。内置 AI 助手可实时总结网页、跨标签页对比信息、自动执行任务。告别繁琐操作,让 AI 成为你的浏览副驾,大幅提升研究与工作效率。支持 Windows、macOS、Android 和 iOS。
下载
关键要点说明:
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)的实践模式。
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))
}