Go httputil.ReverseProxy.Rewrite 怎样重写转发请求
我第一次把 Go 反向代理接进一个已有 HTTP 服务时,真正卡住的不是“怎么把请求转发出去”,而是改写对象经常选错:路由要改目标地址,Host 又要不要保留,客户端带来的 X-Forwarded-For 能不能直接信任。现在更稳妥的做法是直接使用 httputil.ReverseProxy 的 Rewrite,把入站请求和出站请求分开看。
官方资料:https://pkg.go.dev/net/http/httputil#ReverseProxy
下面用一个很小的代理项目串起完整写法:客户端访问代理,代理把路径转到后端服务,并由 Rewrite 统一处理目标 URL、Host 和转发头。这里的重点是代码边界和使用条件,不把示例当成已经在某台机器上运行过的证据。
先确定这个小项目要改什么
项目假设后端服务位于 http://127.0.0.1:9000,代理监听 :8080。客户端请求 /api/users?id=7 时,代理把它送到后端的 /internal/api/users?id=7。
在 Rewrite 回调中,ProxyRequest.In 表示收到的原始请求,应该把它当作只读输入;ProxyRequest.Out 才是准备交给 Transport 的出站请求,可以修改,也可以替换。这个区分是后面所有代码的基础。

准备目标地址和代理 Handler
先解析固定的后端地址,再构造一个 ReverseProxy。解析失败时直接返回错误,避免把一个不完整的 URL 交给代理。
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func newProxy() (*httputil.ReverseProxy, error) {
// 目标地址只描述后端的协议、主机和基础路径。
target, err := url.Parse("http://127.0.0.1:9000/internal")
if err != nil {
return nil, err
}
proxy := &httputil.ReverseProxy{
Rewrite: func(pr *httputil.ProxyRequest) {
// 后续只改写 Out;In 用来读取原始 Host、协议等信息。
pr.SetURL(target)
pr.Out.Host = pr.In.Host
pr.SetXForwarded()
},
}
return proxy, nil
}
func main() {
// 代理本身是一个标准 Handler,可以挂到已有路由树。
proxy, err := newProxy()
if err != nil {
log.Fatal(err)
}
log.Fatal(http.ListenAndServe(":8080", proxy))
}
Rewrite 和 Director 只能二选一,不能在同一个 ReverseProxy 中同时设置。上面的 SetURL 会把目标的 scheme、host 和基础路径写入出站 URL,并按规则拼接入站路径;它默认还会把出站 Host 改成目标主机,所以示例随后显式保留了客户端请求的 Host。
Rewrite 里到底该改哪个请求
我比较容易犯的错误是把 pr.In.URL 当成“待发送的请求”来改。这样做不仅破坏了对象边界,也让后续代码很难判断哪些信息来自客户端、哪些信息已经被代理重写。更清晰的判断方式只有一句:从 In 读取,向 Out 写入。
Rewrite: func(pr *httputil.ProxyRequest) {
// In 是原始请求,只读取客户端需要保留的上下文。
originalHost := pr.In.Host
// SetURL 修改 Out 的目标协议、主机和基础路径。
pr.SetURL(target)
// 需要后端按原始域名路由时,覆盖 SetURL 默认生成的 Host。
pr.Out.Host = originalHost
// 回调返回后不要再访问 pr;代理会继续使用 Out 发送请求。
}
官方文档还特别强调,Rewrite 返回后不能继续访问传入的 ProxyRequest 及其内容。因此不要把 pr 保存到异步 goroutine,也不要在回调结束后再读取 pr.Out 作为“日志快照”。如果需要记录信息,应在回调中提取必要字段后保存独立的值。
路径拼接和 Host 的两个取舍
如果目标 URL 是 http://127.0.0.1:9000/internal,入站请求路径是 /api/users,SetURL 的效果是把目标路径和入站路径组合为后端路径。它适合“代理统一挂在某个后端前缀下”的场景。
如果你不希望拼接入站路径,而是希望完全控制出站路径,可以在 Rewrite 中直接给 pr.Out.URL.Path 赋值。但此时要同时考虑 RawPath、路径转义和尾部斜杠,不能只凭字符串拼接就认为所有 URL 都安全。普通反向代理优先用 SetURL,只有路径规则很明确时才手动改写。
Host 也有两种常见意图:
- 让后端按目标站点识别请求:保留
SetURL设置的目标 Host。 - 让后端按用户访问的域名分流:在
SetURL后设置pr.Out.Host = pr.In.Host。
这两个选择都可以成立,但必须和后端的虚拟主机、鉴权或租户路由约定一致,不要因为“能转发”就默认其中一个。
Host 和转发头怎样一起处理
使用 Rewrite 时,代理会在回调前移除入站请求中的 Forwarded 和 X-Forwarded-* 头。这样做的好处是客户端不能直接把一组伪造的来源信息原样带到后端。需要建立规范转发信息时,再调用 SetXForwarded。

Rewrite: func(pr *httputil.ProxyRequest) {
// 先确定后端路由;SetURL 默认会同步调整出站 Host。
pr.SetURL(target)
// 业务按原始域名路由时再保留入站 Host。
pr.Out.Host = pr.In.Host
// 根据入站连接重建来源 IP、原始 Host 和协议。
pr.SetXForwarded()
}
SetXForwarded 会设置 X-Forwarded-For、X-Forwarded-Host 和 X-Forwarded-Proto。如果代理位于另一层受信任代理之后,并且确实需要把已有的 X-Forwarded-For 链继续传下去,应先按信任边界复制入站值,再调用 SetXForwarded;不要无条件相信来自公网客户端的同名头。
Rewrite: func(pr *httputil.ProxyRequest) {
pr.SetURL(target)
// 只有确认上游代理可信时,才把它的来源链交给 SetXForwarded 追加。
trustedChain := pr.In.Header.Get("X-Forwarded-For")
if trustedChain != "" {
pr.Out.Header.Set("X-Forwarded-For", trustedChain)
}
pr.SetXForwarded()
}
查询参数和 Director 迁移时要留意什么
Rewrite 被调用前,不可解析的查询参数会从出站请求中清理。如果业务必须保留原始的 RawQuery 字符串,可以在回调中从 In.URL.RawQuery 复制到 Out.URL.RawQuery,但要先确认代理与后端对重复键、编码和分隔符的解释一致;否则可能形成参数解析差异。
Rewrite: func(pr *httputil.ProxyRequest) {
pr.SetURL(target)
// 仅在上下游明确约定原始查询串语义时才完整复制。
pr.Out.URL.RawQuery = pr.In.URL.RawQuery
}
从旧代码迁移时,另一个坑是把 Director 的写法原封不动搬过来。官方文档把 Director 标记为弃用方向;它返回后仍可能发生 hop-by-hop header 清理,导致你在 Director 中新增的某些头被移除。使用 Rewrite 可以在预处理完成后修改出站请求,让需要保留的改动处在更合适的位置。
把小项目接入现有服务时的验收清单
把代理挂到真实服务前,我会按下面几项逐一确认:
- 目标 URL 能被正确解析,基础路径是否应该参与拼接已经明确。
- 回调只从
In读取,只向Out写入,没有把ProxyRequest带出回调生命周期。 Rewrite与Director没有同时配置。- Host 的取值符合后端虚拟主机或租户路由约定。
X-Forwarded-For的来源链有明确的信任边界,未直接接受公网伪造值。- 只有确实需要保留原始查询串时才复制
RawQuery,并记录上下游解析约定。 - 需要 WebSocket 或其他协议升级时,额外检查 Transport 与后端能力,不把普通 HTTP 代理示例当成全部配置。
如果只是单一后端、单一基础路径,SetURL 通常已经足够;如果路由规则、Host 规则和来源头规则开始互相交织,就把每一项拆成独立的小函数,让 Rewrite 只负责组装,后续排查会轻松很多。
常见相关问题
Rewrite 可以同时设置 Director 吗?
不可以。两者最多设置一个;同一实例同时设置或同时为空都会让代理无法按预期工作。
SetURL 后为什么后端看到的 Host 变了?
因为 SetURL 默认会把出站 Host 改成目标主机。如果后端需要原始 Host,在 SetURL 后显式设置 pr.Out.Host = pr.In.Host。
能不能在 goroutine 里继续使用 ProxyRequest?
不建议也不应这样做。Rewrite 返回后不能再访问它及其内容;异步逻辑应复制出独立的字符串、数值或 Header 快照。
为什么不直接复制客户端的 X-Forwarded-For?
因为客户端可以伪造它。先定义可信上游,再决定是否复制已有链,并用 SetXForwarded 生成当前代理看到的来源信息。
归纳起来,Rewrite 的核心不是“把 URL 改一下”,而是把入站事实、出站意图和转发元数据分开管理:从 In 取信息,改 Out,用 SetURL 处理常规路由,再明确决定 Host、转发头和查询串的边界。这样一个小型代理才更容易接入已有服务并长期维护。
Go netip.Addr.Is4 怎样判断地址是否为 IPv4
- 上一篇
- Go netip.Addr.Is4 怎样判断地址是否为 IPv4
- 下一篇
- Go netip.Addr.Compare 怎样稳定排序 IP 地址
-
- Golang · Go教程 | 28分钟前 |
- Go bytes.Reader.Seek 怎样回退到可重复读取位置
- 372浏览 收藏
-
- Golang · Go教程 | 29分钟前 | go · bufio · Go bufio.Reader io.EOF ReadByte
- Go bufio.Reader.ReadByte 的单字节读取与 EOF 处理方案
- 154浏览 收藏
-
- Golang · Go教程 | 32分钟前 |
- Go slices.Sorted 怎样把迭代器变成排序后的切片副本
- 234浏览 收藏
-
- Golang · Go教程 | 38分钟前 |
- Go filepath.Rel 怎样判断两个目录是否可相对化
- 201浏览 收藏
-
- Golang · Go教程 | 38分钟前 | Go 空切片 filepath.Glob ErrBadPattern 文件匹配
- Go filepath.Glob 怎样处理无匹配的文件模式
- 316浏览 收藏
-
- Golang · Go教程 | 40分钟前 |
- Go regexp.FindReaderSubmatchIndex 怎样读取流式文本的捕获组
- 385浏览 收藏
-
- Golang · Go教程 | 47分钟前 |
- Go hash/fnv.New32a 怎样为字符串生成稳定哈希
- 491浏览 收藏
-
- Golang · Go教程 | 52分钟前 |
- Go fs.Glob 怎样按模式查找嵌入文件
- 369浏览 收藏
-
- Golang · Go教程 | 54分钟前 |
- Go debug/pe.File.COFFSymbolReadSectionDefAux 怎样读取 COMDAT 辅助符号
- 178浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go net.JoinHostPort 怎样拼接 IPv6 服务地址
- 434浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 709次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 715次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 796次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 754次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 588次使用
-
- Java 性能优化上线清单:从定位、改造到灰度发布
- 2026-06-11 860浏览
-
- Spring Boot 压测验证:Gatling、JMeter 与性能回归门禁
- 2026-06-11 843浏览
-
- Java NMT 非堆内存排查:Direct Buffer、线程栈与 Metaspace 分析
- 2026-06-11 826浏览
-
- Spring Boot 容器内存优化:JVM 堆、非堆与 MaxRAMPercentage
- 2026-06-11 809浏览
-
- Tomcat 连接与线程参数调优:maxThreads、acceptCount 与 KeepAlive
- 2026-06-11 792浏览

