Go 1.26(next)中 net/http 代理环境变量优先级的变更:ProxyFromEnvironment 优先读取小写代理变量
本文基于 Go 标准库的 发布说明文档(对应 golang/go#79656)展开。变更的核心是:net/http 包的 ProxyFromEnvironment 在同时设置了大写和小写代理环境变量、且取值不同时,优先采用小写的 http_proxy、https_proxy、no_proxy,忽略对应的大写版本;这一行为同时延伸影响到 DefaultTransport 和 http.DefaultClient 的默认代理查找逻辑。读完后,你将掌握新版 Go 中代理环境变量的解析优先级、底层实现机制(httpproxy 包与 sync.Once 缓存)、以及如何在自己的代码中验证这一行为。
一、变更内容与影响范围
发布说明的原文要点如下(对应 79656.md):
ProxyFromEnvironmentnow prioritizes lowercase proxy environment variables (http_proxy,https_proxy, andno_proxy) over the uppercase versions if both are set to different values. This change also affects default proxy lookups inDefaultTransportandDefaultClientby extension.
翻译并拆解为两条影响:
- 直接对象:
net/http.ProxyFromEnvironment这一公开函数。当同一组代理的大小写变量被设置为不同的值时,小写变量胜出;若只有其中一种大小写被设置,则自然使用被设置的那一个。 - 间接对象:
http.DefaultTransport和http.DefaultClient。它们默认使用ProxyFromEnvironment作为代理查找函数(见下文源码),因此所有"裸用"http.Get、http.DefaultClient.Do等 API 的用户都会自动获得新的优先级行为,无需修改任何代码。
这个变更解决的是一个真实的历史痛点:长期以来,Linux 桌面环境、CI 系统和容器镜像中普遍同时存在大写变量(如 HTTP_PROXY,常由系统级配置注入)和小写变量(如 http_proxy,常由 shell 或应用注入)。旧版本对两者同时存在时如何取舍没有明确的"谁优先"语义,导致不同程序(curl、wget、Python、Go 各自实现不一)行为不一致,调试代理问题时尤其棘手。现在 Go 给出了明确答案:小写优先。
二、源码层面的实现印证
2.1 ProxyFromEnvironment:公开入口及其文档注释
ProxyFromEnvironment 定义在 src/net/http/transport.go:
// ProxyFromEnvironment returns the URL of the proxy to use for a
// given request, as indicated by the environment variables
// HTTP_PROXY, HTTPS_PROXY and NO_PROXY (or the lowercase versions
// thereof, which take precedence over the uppercase versions).
// Requests use the proxy from the environment variable
// matching their scheme, unless excluded by NO_PROXY.
//
// The environment values may be either a complete URL or a
// "host[:port]", in which case the "http" scheme is assumed.
// An error is returned if the value is a different form.
//
// A nil URL and nil error are returned if no proxy is defined in the
// environment, or a proxy should not be used for the given request,
// as defined by NO_PROXY.
//
// As a special case, if req.URL.Host is "localhost" (with or without
// a port number), then a nil URL and nil error will be returned.
func ProxyFromEnvironment(req *Request) (*url.URL, error) {
return envProxyFunc()(req.URL)
}
从文档注释可以直接确认新版语义:"or the lowercase versions thereof, which take precedence over the uppercase versions"(小写版本优先于大写版本)。函数本身只做一件事——把请求的 *url.URL 交给内部缓存的 envProxyFunc() 返回的闭包处理,即解析全部逻辑都下沉到了代理函数内部。
同一区域的 ProxyURL 提供了一个对照:如果你想固定使用某个代理而不读环境变量,可以把它作为 Transport.Proxy 的函数值使用。
2.2 真正的解析逻辑来自 x/net/http/httpproxy
继续往下看 transport.go 的私有实现部分:
var (
envProxyOnce sync.Once
envProxyFuncValue func(*url.URL) (*url.URL, error)
)
// envProxyFunc returns a function that reads the
// environment variable to determine the proxy address.
func envProxyFunc() func(*url.URL) (*url.URL, error) {
envProxyOnce.Do(func() {
envProxyFuncValue = httpproxy.FromEnvironment().ProxyFunc()
})
return envProxyFuncValue
}
这里有两个关键事实:
- 大小写优先级的具体实现委托给了
golang.org/x/net/http/httpproxy(见 transport.go 顶部 import 中的"golang.org/x/net/http/httpproxy")。httpproxy.FromEnvironment()在进程启动后首次调用时一次性读取所有代理环境变量(http_proxy/HTTP_PROXY、https_proxy/HTTPS_PROXY、no_proxy/NO_PROXY),ProxyFunc()返回一个纯函数,后续调用不再重复读环境变量。 - 结果被
sync.Once缓存(envProxyOnce)。这意味着代理配置在首次使用ProxyFromEnvironment(或任何默认 Transport 发起请求)时即被冻结:运行期间再通过os.Setenv修改http_proxy不会影响已运行的DefaultTransport。仓库中甚至为此保留了测试专用重置函数resetProxyConfig()(transport.go),可见"环境变量只读一次"是刻意设计的行为。对需要动态代理切换的读者,正确做法是自建Transport并在需要时重新构建httpproxy函数或调用ProxyURL,而不是指望修改环境变量生效。
2.3 DefaultTransport 与 DefaultClient 的"by extension"
变更说明中"也影响 DefaultTransport 和 DefaultClient"这句话,在源码中对应 transport.go:
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: defaultTransportDialContext(&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
DefaultTransport 的 Proxy 字段直接指向包级函数 ProxyFromEnvironment;而 DefaultClient(Client 的零值 Transport 回退到 DefaultTransport)因此一并继承该行为。这就是发布说明所谓 "by extension"(延伸影响)的确切含义:只要你没有自己设置 Transport.Proxy,就一定会受本次优先级变更影响。
请求走代理时的调用链为:Transport.RoundTrip → connectMethodForRequest(在 transport.go 中调用 t.Proxy(treq.Request))→ ProxyFromEnvironment → envProxyFunc()(req.URL)。
三、行为细节与边界情况
结合 ProxyFromEnvironment 的文档注释与 [httpproxy 的使用约定],完整的代理选择规则如下:
| 环境变量对 | 匹配的条件 | 说明 |
|---|---|---|
http_proxy / HTTP_PROXY |
请求 scheme 为 http 时 |
小写优先;值可以是完整 URL,也可以是 host[:port](默认按 http scheme 处理) |
https_proxy / HTTPS_PROXY |
请求 scheme 为 https 时 |
小写优先;规则同上 |
no_proxy / NO_PROXY |
对上述代理的例外 | 小写优先;命中例外时返回 nil URL(不走代理) |
其他值得注意的边界:
- localhost 特例:当
req.URL.Host为localhost(带不带端口均可)时,无论环境变量怎么设置,ProxyFromEnvironment都返回nil, nil,即本机地址永不走代理。 - 取值格式错误:环境变量既不是合法 URL 也不是
host[:port]形式时,函数返回错误,该请求随之中止(错误经由connectMethodForRequest传回 RoundTrip)。 - CGI 环境安全限制:
HTTP_PROXY这类变量在 CGI 环境中可能被攻击者通过 HTTP 请求头注入(HTTP_前缀是 CGI 规范中请求头到环境变量的映射规则),因此 Go 会拒绝在 CGI 环境中使用它。测试用例在 src/net/http/transport_test.go 中有明确断言:"refusing to use HTTP_PROXY value in CGI environment"。这一安全约束与本次优先级变更相互独立,升级后依然生效。 - scheme 匹配是大小写不敏感的:
http_proxy与https_proxy各管各的 scheme,一个http://请求不会去读https_proxy。
四、如何用测试用例验证新行为
仓库中 src/net/http/transport_test.go 保留了成对的测试函数,分别覆盖大写与小写路径:
func TestProxyFromEnvironment(t *testing.T) {
// 对每个用例 os.Setenv("HTTP_PROXY", tt.env) 后调用 ProxyFromEnvironment
}
func TestProxyFromEnvironmentLowerCase(t *testing.T) {
// 对每个用例 os.Setenv("http_proxy", tt.env) 后调用 ProxyFromEnvironment
}
两个测试共享同一套用例表(scheme、host、预期代理、预期错误),只是注入的环境变量大小写不同,这保证了大小写两套变量各自独立可用的回归覆盖。对于"大小写同时设置且值不同"这一新语义的具体断言,则由 httpproxy 包(vendored 依赖,见 src 下 golang.org/x/net 的 vendored 目录)负责实现与测试。
如果你在自己的项目里想做一个最小验证程序(新建项目,不要修改本仓库):
package main
import (
"fmt"
"net/http"
)
func main() {
// 进程启动前设置:
// HTTP_PROXY=http://upper-proxy:8080
// http_proxy=http://lower-proxy:8080
req, _ := http.NewRequest("GET", "http://example.com/", nil)
u, err := http.ProxyFromEnvironment(req)
fmt.Println(u, err) // 新行为:输出 lower-proxy:8080,而非 upper-proxy
}
预期输出小写的 lower-proxy,即可确认工具链已包含本变更。
五、升级建议与注意事项
- 只有一边设置了代理变量:行为完全不变,无需任何改动。
- 两边设置了相同的值:结果不变,优先级此时无实际影响。
- 两边设置了不同的值:这是唯一行为发生变化的场景——升级后小写值生效。如果这正是你期望的行为(多数 shell 场景下
http_proxy是"用户/应用最近一次显式设置"的),那这就是一个 bug 修复;如果你的环境刻意依赖大写值覆盖小写值(例如镜像中烘焙了http_proxy,运行时想通过大写HTTP_PROXY覆盖),需要调整环境注入顺序或统一只保留一个大小写的变量。 - 同一进程内动态修改环境变量无效:如前所述,
envProxyFunc经sync.Once缓存后,环境变量在首次解析后即被冻结;这是既有设计,与本次变更无关,但排查"为什么改了http_proxy却没生效"时是首要怀疑点。 - 自建有代理逻辑的代码:如果你的服务实现了自定义
Proxy func(*Request) (*url.URL, error)(赋值给Transport.Proxy),则不会受到本次变更影响;只有默认路径(ProxyFromEnvironment/DefaultTransport/DefaultClient)行为改变。
六、参考文件索引
| 内容 | 路径 |
|---|---|
| 本变更的发布说明 | doc/next/6-stdlib/99-minor/net/http/79656.md |
ProxyFromEnvironment / ProxyURL 定义 |
src/net/http/transport.go |
DefaultTransport 默认使用 ProxyFromEnvironment |
src/net/http/transport.go |
envProxyFunc(httpproxy + sync.Once 缓存) |
src/net/http/transport.go |
| 代理选择与 CGI 限制的测试 | src/net/http/transport_test.go |
| 同系列其他 net/http 变更说明 | 79040.md、80058.md |
总结:这是 net/http 默认代理路径上一个语义明确化的变更——小写代理环境变量(http_proxy、https_proxy、no_proxy)在与大写版本冲突时优先,解析逻辑统一收敛于 httpproxy 包并经 sync.Once 缓存,DefaultTransport 与 http.DefaultClient 因默认绑定 ProxyFromEnvironment 而自动继承新行为。对绝大多数用户这是向 curl/wget 等工具看齐的兼容性改进;仅当你的部署环境刻意依赖"大写覆盖小写"时才需要相应调整。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00