Go 网络编程¶
学习目标¶
学完本章后,学习者应该能够:
- 使用
net和net/http编写基础 TCP 与 HTTP 服务。 - 正确配置 HTTP Client 连接池和 Timeout。
- 理解 Go DNS 解析行为和 context 取消。
- 能避免常见网络泄漏与连接泄漏。
net 与 net/http¶
net 包提供 TCP、UDP、Unix Socket、DNS 等底层能力。
net/http 构建在更高层,提供 HTTP Server、Client、Transport、Handler 等能力。
日常后端开发优先使用 net/http 或成熟框架;只有需要自定义协议时,才直接使用 net。
HTTP Client 连接池¶
Go 的 http.Client 应该复用,不要每次请求都创建新的 Client。
var client = &http.Client{
Timeout: 3 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
IdleConnTimeout: 90 * time.Second,
},
}
频繁创建 Client 和 Transport 会破坏连接复用,增加短连接、TLS 握手和端口压力。
Timeout 设置¶
常见超时:
- 整体请求超时。
- 连接建立超时。
- TLS 握手超时。
- 响应头超时。
- 空闲连接超时。
- 服务端读写超时。
超时不是越长越好。它应该符合业务 SLA 和调用链总预算。
DNS 解析行为¶
Go 可能使用纯 Go resolver,也可能使用 cgo resolver,取决于系统、环境变量和构建方式。
工程上更重要的是:
- DNS 可能慢。
- DNS 可能失败。
- 长连接复用可能减少解析次数。
- Kubernetes 内部 DNS 依赖 CoreDNS。
Go 后端实际应用例子¶
例子一:生产可用的 HTTP Client 工厂¶
func NewHTTPClient(timeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: 200,
MaxIdleConnsPerHost: 50,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 3 * time.Second,
ResponseHeaderTimeout: 2 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
}
具体数值要根据业务 SLA、下游能力和压测结果调整。
例子二:正确处理响应体¶
func Get(ctx context.Context, client *http.Client, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, resp.Body)
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
响应体要关闭;大响应不要无脑 io.ReadAll,应限制大小或流式处理。
例子三:简单 TCP Server¶
func ServeTCP(addr string) error {
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
defer ln.Close()
for {
conn, err := ln.Accept()
if err != nil {
return err
}
go func() {
defer conn.Close()
_, _ = io.Copy(conn, conn)
}()
}
}
真实服务还要增加超时、并发限制、协议解析、错误处理和优雅退出。
常见误区¶
- 误区一:每次请求 new 一个 http.Client 更安全。
这会破坏连接池,导致短连接和端口压力。应复用 Client。
- 误区二:只设置 context 超时就够。
context 很重要,但 Transport 层也需要合理配置连接、TLS、响应头等超时。
- 误区三:响应体小就不用关闭。
只要 client.Do 成功返回响应,就应该关闭 body,否则可能影响连接复用或造成泄漏。
线上问题案例¶
某服务每次请求第三方 API 都创建新的 http.Client。流量上涨后,机器出现大量 TIME_WAIT,端口压力和 TLS 握手耗时上升。
修复方式是复用全局 Client,配置连接池和超时,并监控连接状态、请求耗时分段和错误类型。
实战任务¶
封装一个生产可用的 HTTP 调用函数:
- 复用 HTTP Client。
- 支持 context 超时。
- 正确关闭响应体。
- 对非 2xx 状态返回错误。
- 记录耗时和目标 URL。
参考答案
可以把 http.Client 作为依赖注入到调用函数或结构体中,避免每次创建。请求使用 http.NewRequestWithContext,由上游传入带超时的 context。client.Do 成功返回后必须 defer resp.Body.Close()。
对非 2xx 状态,可以读取有限大小的错误响应体用于日志,再返回包含状态码的错误。调用前后记录目标 host、path、耗时、状态码和错误类型,避免日志泄露敏感 query 或 token。
面试题¶
1. 为什么 http.Client 应该复用?¶
参考答案
http.Client 内部通过 Transport 管理连接池。复用 Client 可以复用 TCP 连接和 TLS 会话,减少建连成本、端口压力和延迟。
每次请求都创建新的 Client 或 Transport,容易导致大量短连接、TIME_WAIT 增多和性能下降。
2. Go HTTP 请求有哪些常见超时?¶
参考答案
常见超时包括整体请求超时、连接建立超时、TLS 握手超时、响应头超时、空闲连接超时,以及服务端的读取 Header、读取 Body、写响应超时。
超时要结合业务 SLA 和调用链预算配置,不能无限等待,也不能短到正常请求经常误伤。
3. 如何避免 Go 网络连接泄漏?¶
参考答案
关键是复用 Client、设置超时、使用 context、确保响应体和连接在所有路径关闭、限制并发、监控连接状态和文件描述符数量。
对 HTTP Client,只要 Do 成功返回响应,就必须关闭 resp.Body。对自定义 TCP 连接,要在退出路径 Close,并设置读写 deadline 防止永久阻塞。