登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go net 怎样用 Resolver 控制 DNS 解析策略

来源:17golang原创

时间:2026-10-24 12:21:35 222浏览 收藏

Go 可以为每个 net.Resolver 设置解析策略,再通过 net.Dialer.Resolver 接到指定客户端。PreferGo 负责在支持的平台优先使用 Go 内置解析实现,Dial 负责 Go 解析器连接 DNS 服务时的拨号方式;前者不是 DNS 地址开关,后者也不是业务服务器连接函数。

只想查询域名时,直接调用该对象的 Lookup 方法;希望 HTTP 请求也采用该策略时,还需要把带 Resolver 的 Dialer 接到 Transport。以下依据 net.Resolver 官方文档及 http.Transport 文档说明。示例未在本地执行,不提供虚构的解析结果或延迟数据。

为何把 DNS 策略从全局收回客户端

一个进程可能同时访问企业内网服务与外部接口。修改全局解析器会影响原本不需要改变的组件,而且依赖库可能有自己的连接实现。对象级配置的价值是让策略归属清楚:哪个 Resolver 配哪类查询,哪个客户端使用哪个 Dialer。这里讨论的是可选择的工程组织方式,不声称某种方案正在成为行业热门。

当系统已有分域解析、VPN、目录服务或特殊名字服务规则时,保留系统默认通常更容易继承这些能力。只有明确需要 Go 解析实现、特定可达 DNS 服务和独立客户端策略时,才考虑下文配置。它不是无条件更快的选项。

先选解析实现,再选 DNS 服务地址

配置控制内容不代表什么
PreferGo在支持的平台优先采用 Go 内置解析器,作用域是当前对象不自动改成某个公共 DNS,也不承诺完全复制系统原生规则
DialGo 解析器到 DNS 服务的 TCP/UDP 连接不是业务 HTTP 请求的目标连接,不直接返回 IP 列表
StrictErrorsGo 解析器遇到部分临时错误时,是否中止整个组合查询不是 DNSSEC、加密 DNS 或安全等级开关

Resolver 的 Dial 回调收到的 address 使用字面 IP 和字面端口。想将 DNS 网络请求定向到固定服务器,可以忽略该 address,改用配置好的 IP:端口,但应保留传入的 network。不能把 network 永远写死成 udp,否则需要 TCP 的查询就无法按预期进行。零值 Resolver 和 nil Resolver 指针具有等价的默认含义。

Resolver 配置与 DNS 连接的静态关系
结构图:PreferGo、StrictErrors 属于解析策略,Dial 连接的是 DNS 服务,不是业务服务器。

为独立客户端构造解析器

下面的构造函数要求调用者提供受信任、实际可达的 DNS IP 和端口。解析器内部的 DNS 拨号器不再持有这个自定义 Resolver,地址又是 IP,避免为了寻找 DNS 服务器先递归查询其域名。构造时检查的是配置格式,不会发起网络访问。

package dnsclient

import (
    "context"
    "fmt"
    "net"
    "net/http"
    "net/netip"
    "strconv"
    "time"
)

// NewResolver 建立对象级策略;dnsIP 必须来自受信任的部署配置。
func NewResolver(dnsIP string, port uint16) (*net.Resolver, error) {
    addr, err := netip.ParseAddr(dnsIP)
    if err != nil || port == 0 {
        return nil, fmt.Errorf("DNS IP 或端口配置无效")
    }
    server := net.JoinHostPort(addr.String(), strconv.Itoa(int(port)))
    dnsDialer := &net.Dialer{Timeout: 2 * time.Second}
    return &net.Resolver{
        PreferGo: true,
        // 保留默认的部分结果兼容行为;有明确需求再启用严格错误。
        StrictErrors: false,
        Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
            // 保留解析器指定的 UDP/TCP,并向下传递取消信号。
            return dnsDialer.DialContext(ctx, network, server)
        },
    }, nil
}

// Lookup 使用同一个 Resolver,并为这一次查询设置总时间预算。
func Lookup(parent context.Context, r *net.Resolver, host string) ([]net.IPAddr, error) {
    ctx, cancel := context.WithTimeout(parent, 3*time.Second)
    defer cancel()
    ips, err := r.LookupIPAddr(ctx, host)
    if err != nil {
        return nil, fmt.Errorf("域名解析失败: %w", err)
    }
    return ips, nil
}

// NewDirectClient 只用于允许直接访问目标服务的部署环境。
func NewDirectClient(r *net.Resolver) (*http.Client, *http.Transport) {
    appDialer := &net.Dialer{
        Timeout: 5 * time.Second,
        KeepAlive: 30 * time.Second,
        Resolver: r,
    }
    tr := http.DefaultTransport.(*http.Transport).Clone()
    // 明确禁用代理,只为让本示例的域名解析位置可知。
    tr.Proxy = nil
    tr.DialContext = appDialer.DialContext
    return &http.Client{Transport: tr, Timeout: 12 * time.Second}, tr
}

示例中的二秒、三秒、五秒、十二秒是不同层的预算示例,不是推荐给所有业务的固定值。DNS Dialer.Timeout 约束建连,不能简单当成 DNS 整次查询的耗时上限;Lookup 的 context 约束这次查询,业务 Dialer 的预算还涉及解析与连接,HTTP Client.Timeout 则覆盖请求生命周期,包括读取响应体。应按服务的实际延迟目标协调配置。

判断这段配置是否接对,不是看某个开关打印为 true,而是确认实际调用了返回的 Resolver 或 Client。继续使用包级 net.LookupIP 或另一个客户端,不会自动采用这里新建的对象。将对象在初始化时构造好并复用,不要一边供并发请求使用,一边改字段。

把解析器接到 HTTP 连接上

上例存在两种不同的 Dialer:DNS 拨号器负责 DNS 服务连接;业务拨号器负责最终目标连接,并引用 Resolver。Transport 的 DialContext 使用业务拨号器,Client 再持有 Transport。仅替换 Resolver.Dial 而没有把 Resolver 绑定到业务拨号器,并不能改变一个已有 HTTP 客户端的解析行为。

HTTP 客户端、Transport、业务 Dialer 与 DNS Resolver 的依赖边界
结构图:外层客户端连接业务服务,Resolver 内部拨号器连接 DNS 服务,两个地址不能混用。

返回 Transport 是为了在客户端退役时调用 CloseIdleConnections。普通请求要正常关闭响应体;连接池应复用,不要每次请求结束就销毁整个 Transport。DNS 策略只在需要解析并建立新连接时参与,复用已有连接的请求不一定触发新查询。

保持 URL 中的业务域名,不要为了“指定 DNS”先把它改写成 IP。HTTPS 的证书名称校验和 SNI 仍应围绕正确的域名配置,不能用关闭证书验证来掩盖名称不匹配。自定义解析器不是绕开 TLS 校验的理由。

不要让严格错误与代理改变预期

StrictErrors 影响 A 与 AAAA、搜索列表等组合查询:某些子查询发生超时、套接字错误或 SERVFAIL 时,开启它可能使原本能返回部分地址的查询整体失败。因此默认 false 不是遗漏配置,true 也不是通用“生产必选”。需要完整结果的业务与可以接受部分结果的业务,取舍可能不同。

Resolver.Dial 控制的是发往 DNS 服务的连接,不保证每次名字查询都产生网络请求。例如本地 hosts 命中或字面 IP 输入就不能按“必定访问指定 DNS”理解。固定一个服务器也不等于重建全部系统搜索、hosts 和名字服务逻辑。改用 Go 解析器前,应明确系统原生规则是否属于业务依赖。

另外,示例把 Proxy 设为 nil 是明确的直连边界,不应在强制代理的环境照搬。使用 HTTP 或 SOCKS 代理时,目标域名可能交给代理侧处理,本地 DialContext 所连接和解析的对象也可能是代理服务器。要控制实际 DNS 位置,必须先明确连接架构;不要把本地 Lookup 成功等同于代理请求走了同一解析路径。

采用与观察应分开

建议先为一类受控请求建立独立客户端,保留原来的默认客户端作为配置回退路径。切换时创建新对象,让旧请求自然结束,再清理旧 Transport 的空闲连接;不要在并发使用中替换全局 DefaultResolver。这里给出的是采用建议,不是已实施的生产结论。

部署后可以按解析耗时、错误类别、请求连接耗时和成功率分开观察,并注明 DNS 配置版本。httptrace 的 DNSStart、DNSDone 和 GotConn 可帮助区分解析与连接复用;日志不宜记录完整敏感内网域名列表。不要用一次查到地址或某次低延迟推导“新方案更稳定”。本次文章生产没有执行这些诊断或测试。

PreferGo 会设置固定 DNS 吗?

不会。固定服务地址来自自定义 Dial 或系统解析配置;PreferGo 选择的是解析实现的偏好。

Dial 能直接返回一个 IP 地址吗?

不能。它需要返回符合 DNS 传输要求的 net.Conn,由解析器收发 DNS 消息并解释响应。

更换 Resolver 后旧 HTTP 连接会立即重查 DNS 吗?

不会据此强制重查。已有连接可能继续复用,应按客户端与连接池的生命周期规划配置切换。

因此,控制 DNS 解析策略的关键是分清三层:解析实现、DNS 服务连接、业务客户端连接。PreferGo、Resolver.Dial 与 Dialer.Resolver 分别落到相应位置,才能让配置范围与实际请求路径保持一致。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>