Go 怎么用 httptest.NewServer 测试重定向链
测试 HTTP 重定向时,最容易漏掉的不是 302 状态码,而是客户端到底访问了哪几跳。用 httptest.NewServer 搭一个只存在于测试期间的本地服务,再用 http.Client.CheckRedirect 记录目标请求,就能把“是否跟随”和“跟随到哪里”分开断言。
默认客户端会继续跟随重定向;想测试完整链路,就在服务端记录路径或在CheckRedirect中记录req。想停在当前 3xx,则返回http.ErrUseLastResponse,并主动关闭返回的响应体。
httptest.NewServer适合构造稳定、可关闭的重定向测试入口。- 服务端路径记录能验证真实到达链路,
CheckRedirect能验证客户端准备跟随的下一跳。 ErrUseLastResponse会返回当前响应,不能把它当作普通错误处理。
先把每一跳写成可观察的测试服务器
先定义三段路径:/start 跳到 /middle,/middle 再跳到 /final,最后返回 200。测试只关心重定向结构,所以不需要启动真实端口或依赖外部站点;NewServer 会返回带随机本地地址的测试服务器,结束时用 Close 释放它。
func newRedirectServer(t *testing.T, seen *[]string) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
*seen = append(*seen, r.URL.Path) // 记录真实到达的每一跳
switch r.URL.Path {
case "/start":
http.Redirect(w, r, "/middle", http.StatusFound) // 第一跳
case "/middle":
http.Redirect(w, r, "/final", http.StatusTemporaryRedirect) // 第二跳
case "/final":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("done")) // 终点只返回一个可断言结果
default:
http.NotFound(w, r)
}
}))
}
这里混用 302 和 307,是为了让测试明确知道每个节点返回什么;如果只验证 GET,二者都可以被客户端继续处理。seen 保存的是服务器实际收到的路径,适合断言最终链路,而不是猜测客户端行为。

默认客户端如何断言完整重定向链
服务器准备好后,直接使用 ts.Client() 发起请求。这个客户端复用了测试服务器的传输配置;默认的 CheckRedirect 允许客户端继续跟随,直到最终响应或连续重定向达到上限。
func TestRedirectChain(t *testing.T) {
var seen []string
ts := newRedirectServer(t, &seen)
defer ts.Close() // 测试结束关闭监听器和连接
client := ts.Client()
resp, err := client.Get(ts.URL + "/start") // 默认跟随重定向
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("最终状态码 = %d, want %d", resp.StatusCode, http.StatusOK)
}
if got, want := string(mustRead(t, resp.Body)), "done"; got != want {
t.Fatalf("最终响应体 = %q, want %q", got, want)
}
wantPaths := []string{"/start", "/middle", "/final"}
if !reflect.DeepEqual(seen, wantPaths) {
t.Fatalf("到达路径 = %#v, want %#v", seen, wantPaths)
}
}
示例中的 mustRead 只是把 io.ReadAll 的错误转成测试失败;实际文件中要补上 io 和 reflect 的导入。这个断言组合了最终状态、响应体和完整路径,任何一跳丢失都会暴露出来。生产代码若使用 POST,还要额外断言 301/302/303 与 307/308 对方法和请求体的不同语义。
用 CheckRedirect 观察客户端准备发出的请求
服务端记录回答“请求实际到了哪里”,而 CheckRedirect 回调回答“客户端准备跟随到哪里”。回调参数里的 req 是即将发出的请求,via 按最早到最近保存已经发出的请求。两者一起记录,定位错误跳转会更直接。
func TestRedirectTargets(t *testing.T) {
var seen []string
ts := newRedirectServer(t, &seen)
defer ts.Close()
var targets []string
client := ts.Client()
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
targets = append(targets, req.URL.Path) // 记录下一跳,不是当前响应路径
if len(via) > 2 {
return fmt.Errorf("重定向次数异常: %d", len(via)) // 提前暴露循环
}
return nil
}
resp, err := client.Get(ts.URL + "/start")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
want := []string{"/middle", "/final"}
if !reflect.DeepEqual(targets, want) {
t.Fatalf("下一跳 = %#v, want %#v", targets, want)
}
}
回调不会在初始请求前执行,所以 targets 从 /middle 开始;初始的 /start 仍由服务端 seen 记录。若回调返回普通错误,客户端会停止并返回一个包装后的 *url.Error,这适合把不允许的跨域或过长链路变成明确失败。
禁止跟随时怎么检查每一跳
有些单元测试并不想访问终点,只想检查当前响应的 StatusCode 和 Location。这时返回 http.ErrUseLastResponse,客户端会把最近一份响应交给调用方。它与普通 CheckRedirect 错误不同:这里得到的是当前响应和 nil error,因此要按响应路径读取并关闭 Body。
func TestStopAtFirstRedirect(t *testing.T) {
ts := newRedirectServer(t, &[]string{})
defer ts.Close()
client := ts.Client()
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // 保留当前 302,不请求 /middle
}
resp, err := client.Get(ts.URL + "/start")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close() // ErrUseLastResponse 返回的 Body 不会替你关闭
if resp.StatusCode != http.StatusFound {
t.Fatalf("状态码 = %d, want %d", resp.StatusCode, http.StatusFound)
}
if got, want := resp.Header.Get("Location"), "/middle"; got != want {
t.Fatalf("Location = %q, want %q", got, want)
}
}
测试重定向时可以按下面的边界选择断言点:
| 目标 | 观察位置 | 关键断言 |
|---|---|---|
| 真实访问链 | Handler | seen 的路径顺序 |
| 下一跳策略 | CheckRedirect | req.URL 与 via 长度 |
| 停止当前跳 | 返回值与响应 | ErrUseLastResponse、3xx、Location |
| 资源清理 | 调用方 | 所有非 nil resp.Body 都关闭 |

常见问题
为什么只断言最终 200 不够?
因为错误的中间跳转也可能最终到达 200。记录 Handler 路径,才能发现多跳、漏跳或意外访问了不该访问的路径。
CheckRedirect 里的 req 是当前请求还是下一请求?
它是客户端准备发出的下一请求;已经发出的请求按时间顺序放在 via 中,所以初始入口不会出现在回调记录里。
返回 ErrUseLastResponse 后为什么还要 Close Body?
这是保留当前响应的特殊分支,响应体交给调用方管理。无论最终是 200 还是 302,都应在读取后关闭 resp.Body。
一条可靠的重定向测试通常同时保留两种视角:用 Handler 记录真实路径,用 CheckRedirect 检查客户端策略;需要停在某一跳时,再用 ErrUseLastResponse 固定断言边界。
Python Decimal 和 float 混用为什么会报类型错误
- 上一篇
- Python Decimal 和 float 混用为什么会报类型错误
- 下一篇
- Linux ext4 文件删除后空间没有释放是什么原因
-
- Golang · Go教程 | 6小时前 | goroutine · pprof · Go教程 · 运行时 · Go pprof 性能诊断 goroutine profile pprof.Lookup
- Go pprof.Lookup 怎样读取 goroutine profile 文本
- 465浏览 收藏
-
- Golang · Go教程 | 6小时前 | 标准库 · HTTP · Go教程 · Go 请求头 textproto CanonicalMIMEHeaderKey MIMEHeader
- Go textproto.CanonicalMIMEHeaderKey 怎样统一请求头键名
- 283浏览 收藏
-
- Golang · Go教程 | 6小时前 |
- Go image.NewRGBA 怎样创建可修改的 RGBA 画布
- 289浏览 收藏
-
- Golang · Go教程 | 6小时前 |
- Go compress/gzip Reader 怎样复用并正确关闭
- 454浏览 收藏
-
- Golang · Go教程 | 6小时前 |
- Go archive/zip FileHeader.ExternalAttrs 读取条目权限属性
- 460浏览 收藏
-
- Golang · Go教程 | 6小时前 | 标准库 · Go教程 · Go 邮箱地址 net/mail ParseAddressList 收件人列表
- Go net/mail ParseAddressList 怎样解析多个收件人
- 465浏览 收藏
-
- Golang · Go教程 | 6小时前 | Go教程 · Go crypto/rand 随机令牌 随机字节 密钥材料
- Go crypto/rand 怎样生成固定字节数随机值
- 443浏览 收藏
-
- Golang · Go教程 | 6小时前 |
- Go net.Dialer 怎样配置连接建立超时
- 151浏览 收藏
-
- Golang · Go教程 | 6小时前 |
- Go tls.Config 怎样限制最低 TLS 版本
- 197浏览 收藏
-
- Golang · Go教程 | 6小时前 | go · Plugin · Go plugin 版本兼容 buildmode=plugin
- Go plugin 包为什么不适合跨版本加载
- 426浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 604次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 644次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 674次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 639次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 460次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- 详解Go 语言如何通过测试保证质量
- 2022-12-29 290浏览
-
- Golang 单元测试和基准测试实例详解
- 2022-12-23 275浏览
-
- GoLang基础学习之go test测试
- 2023-01-01 265浏览
-
- Golang实现HTTP编程请求和响应
- 2022-12-28 101浏览

