又又又学到新东西
1. 先来看一段代码
go
var _ = func() struct{} {
http.DefaultTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment, //支持系统代理
MaxIdleConns: 400, // 最大空闲连接数
MaxIdleConnsPerHost: 80, // 保留80个空闲连接复用
MaxConnsPerHost: 150, // 限制每个主机的最大连接数
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ForceAttemptHTTP2: true,
}
return struct{}{}
}()
乍一看,我去这是什么用法?
是不是很像 var num int 这一种全局变量的用法呢,没错其实这一种用法,和这个全局定义十分类似,这其实是一个匿名自执行函数 + 包级变量初始化的组合技。
拆开来看:
| 部分 | 作用 |
|---|---|
func() struct{} { ... } |
定义一个匿名函数,返回一个空的 struct{} |
(...)() |
立即执行这个匿名函数 |
var _ = |
把执行结果赋值给一个"下划线"变量(丢弃结果,只为触发副作用) |
| 整体 | 在包初始化(package init)阶段就强制执行这个函数 |
结论:这段代码会在当前包被导入或程序启动时,自动执行一次,把 http.DefaultTransport 替换成我们自定义配置好的 Transport。
2. 为什么不直接写成 init() 函数?
有两种常见写法是等价的:
写法1(你看到的这种):
go
var _ = func() struct{} {
http.DefaultTransport = &http.Transport{...}
return struct{}{}
}()
写法2(更直观的):
go
func init() {
http.DefaultTransport = &http.Transport{...}
}
两者执行时机完全一样,但是在真实场景下,还是建议使用第一种方式,原因有几个:
-
多个 init(),他们的执行顺序是不确定的
Go 允许一个包有多个
init()函数,执行顺序不保证。如果你想明确控制"这个初始化必须最先执行",用var _ = func()...()写在文件最上面就能强制排在所有init()前面(因为 var 初始化比 init() 更早)。 -
避免污染 init() 命名空间
有些团队觉得
init()太多会乱,喜欢用这种"无名"方式。 -
看起来更"一次性"
一眼就看出这代码只运行一次,没副作用返回。
3. 这么改 DefaultTransport 有什么实际意义?
http.DefaultClient(也就是大家经常直接用的 http.Get、http.Post)底层就是用的 http.DefaultTransport。
如果你不改,默认值大概是:
go
MaxIdleConns: 100
MaxIdleConnsPerHost: 2 // ←←← 这才是最致命的!!
IdleConnTimeout: 90s
MaxIdleConnsPerHost 默认只有 2!
意味着你同一个主机(比如 api.ai.com),即使开了几百个 goroutine 并发请求,最多也只复用 2 个空闲连接,其他的都会重新建连接 → 性能极差,容易把对端打挂,或者被限流。
改成 80~100 之后,才真正发挥出 Go 高并发优势,典型生产环境配置:
go
MaxIdleConns: 400-1000 // 总空闲连接池大小
MaxIdleConnsPerHost: 50-200 // 单个主机复用连接数(关键!)
MaxConnsPerHost: 0 或很大 // 0 表示不限制(Go 1.11+ 推荐)
一句话总结:这是 Go 高并发服务必备的"全局连接池优化"黑魔法,几乎所有中大型 Go 项目启动文件里都有这一段(或等价的 init())。