跳转至

Go 网络编程

学习目标

学完本章后,学习者应该能够:

  1. 使用 netnet/http 编写基础 TCP 与 HTTP 服务。
  2. 正确配置 HTTP Client 连接池和 Timeout。
  3. 理解 Go DNS 解析行为和 context 取消。
  4. 能避免常见网络泄漏与连接泄漏。

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 调用函数:

  1. 复用 HTTP Client。
  2. 支持 context 超时。
  3. 正确关闭响应体。
  4. 对非 2xx 状态返回错误。
  5. 记录耗时和目标 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 防止永久阻塞。