今天我们全程用自然语言指挥 OpenCode,做一个命令行 DNS 记录查询工具。输入一个域名,就能查询它的 A、AAAA、MX、CNAME、TXT、NS 等 DNS 记录。技术栈只用 Go 标准库 net 包,零第三方依赖。最终代码约 85 行,4 分钟搞定。
go version 确认)打开 OpenCode 终端,我只说了一句:
> 帮我创建一个 Go 项目 dns-lookup,功能是通过命令行参数接收域名,查询并打印该域名的 A 记录(IPv4 地址)。
OpenCode 分析需求后,直接在当前目录下执行 go mod init dns-lookup 初始化模块,然后生成了 main.go:
package main
import (
"context"
"fmt"
"net"
"os"
"time"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: dns-lookup <domain>")
os.Exit(1)
}
domain := os.Args[1]
resolver := &net.Resolver{PreferGo: true}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ips, err := resolver.LookupHost(ctx, domain)
if err != nil {
fmt.Fprintf(os.Stderr, "查询失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("域名: %s\nA 记录:\n", domain)
for _, ip := range ips {
fmt.Printf(" %s\n", ip)
}
}
要点:OpenCode 自动选择了 net.Resolver 而非简单的 net.LookupHost,因为 Resolver 可以设置超时(5 秒)和 Go 原生解析器偏好,比默认函数更健壮。我没提这些细节,AI 自己做了最佳实践选择。
A 记录太单一,我对 OpenCode 说:
> 很好,但只查 A 记录不够。帮我扩展功能,支持 A、AAAA、MX、CNAME、TXT、NS 六种记录类型,通过 -type 参数指定,默认查 A 记录。
OpenCode 拿到指令后,做了三件事:① 引入 flag 包处理命令行参数;② 用 switch 分支分发不同记录类型的查询逻辑;③ 分别调用 resolver 的 LookupHost、LookupMX、LookupCNAME、LookupTXT、LookupNS 等方法,每个方法都是 net 包原生支持的。整个过程 30 秒不到,代码直接可用。
要点:Go 的 net.Resolver 提供了完整的 DNS 查询方法族,同一个 Resolver 实例可以查询所有记录类型,无需任何外部依赖——这是标准库设计优秀的体现,也是我选 Go 做这个工具的原因。
代码逻辑对了,但输出还不够友好。我继续对 OpenCode 说:
> 格式化输出,显示记录类型的中文名称。IPv6 和 IPv4 地址分开显示,不要混在一起。参数支持小写。
OpenCode 分别做了以下优化:
strings.ToUpper 把 -type 参数统一转大写,用户输入 a 或 A 都能工作A (IPv4)、MX (邮件交换)LookupIPAddr),再通过 ip.To4() != nil 判断是 v4 还是 v6,分开输出,避免 IPv6 地址污染 IPv4 结果列表os.Stderr要点:LookupIPAddr 同时返回 IPv4 和 IPv6,区分它们的标准做法是 addr.IP.To4() != nil,这是 IPv4 判定最稳妥的方式。
我对 OpenCode 说:
> 编译并测试几个域名,验证功能正常。
OpenCode 先 go build -o dns-lookup . 编译,然后自动跑了几条验证命令。实际输出如下:
$ ./dns-lookup -type A google.com 域名: google.com 记录类型: A (IPv4) 142.250.80.14 $ ./dns-lookup -type MX qq.com 域名: qq.com 记录类型: MX (邮件交换) mx3.qq.com (优先级: 30) mx2.qq.com (优先级: 20) mx1.qq.com (优先级: 10) $ ./dns-lookup -type NS baidu.com 域名: baidu.com 记录类型: NS (域名服务器) ns.baidu.com ns2.baidu.com $ ./dns-lookup -type txt github.com 域名: github.com 记录类型: TXT (文本) v=spf1 include:_spf.github.com ~all docusign=abcdef-1234-5678
所有记录类型查询正常,工具验证通过。
package main
import (
"context"
"flag"
"fmt"
"net"
"os"
"strings"
"time"
)
func main() {
var recordType string
flag.StringVar(&recordType, "type", "A", "DNS 记录类型: A, AAAA, MX, CNAME, TXT, NS")
flag.Parse()
if flag.NArg() < 1 {
fmt.Println("Usage: dns-lookup [-type A|AAAA|MX|CNAME|TXT|NS] <domain>")
os.Exit(1)
}
domain := flag.Arg(0)
recordType = strings.ToUpper(recordType)
resolver := &net.Resolver{PreferGo: true}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
fmt.Printf("域名: %s\n", domain)
switch recordType {
case "A", "AAAA":
typeName := "A (IPv4)"
if recordType == "AAAA" {
typeName = "AAAA (IPv6)"
}
fmt.Printf("记录类型: %s\n\n", typeName)
addrs, err := resolver.LookupIPAddr(ctx, domain)
if err != nil {
fmt.Fprintf(os.Stderr, "查询失败: %v\n", err)
os.Exit(1)
}
for _, addr := range addrs {
isIPv4 := addr.IP.To4() != nil
if recordType == "A" && isIPv4 {
fmt.Printf(" %s\n", addr.IP)
} else if recordType == "AAAA" && !isIPv4 {
fmt.Printf(" %s\n", addr.IP)
}
}
case "MX":
fmt.Printf("记录类型: MX (邮件交换)\n\n")
records, err := resolver.LookupMX(ctx, domain)
if err != nil {
fmt.Fprintf(os.Stderr, "查询失败: %v\n", err)
os.Exit(1)
}
for _, mx := range records {
fmt.Printf(" %s (优先级: %d)\n", mx.Host, mx.Pref)
}
case "CNAME":
fmt.Printf("记录类型: CNAME (别名)\n\n")
cname, err := resolver.LookupCNAME(ctx, domain)
if err != nil {
fmt.Fprintf(os.Stderr, "查询失败: %v\n", err)
os.Exit(1)
}
fmt.Printf(" %s\n", cname)
case "TXT":
fmt.Printf("记录类型: TXT (文本)\n\n")
records, err := resolver.LookupTXT(ctx, domain)
if err != nil {
fmt.Fprintf(os.Stderr, "查询失败: %v\n", err)
os.Exit(1)
}
for _, txt := range records {
fmt.Printf(" %s\n", txt)
}
case "NS":
fmt.Printf("记录类型: NS (域名服务器)\n\n")
records, err := resolver.LookupNS(ctx, domain)
if err != nil {
fmt.Fprintf(os.Stderr, "查询失败: %v\n", err)
os.Exit(1)
}
for _, ns := range records {
fmt.Printf(" %s\n", ns.Host)
}
default:
fmt.Fprintf(os.Stderr, "不支持的记录类型: %s\n", recordType)
os.Exit(1)
}
}
从"帮我创建一个 Go 项目"到四次交互后完成编译验证,全程不到 4 分钟。核心体会:
net.LookupMX 还是 net.Resolver.LookupMX,AI 会自动选最优方案context.WithTimeout)、Go 原生解析器(PreferGo)、错误输出走 stderr,这些都是 OpenCode 自动加的,而不是我要求的如果手写这个工具,查文档、调试类型断言、处理边界条件,至少需要 20 分钟。用 OpenCode,相当于把查文档和写样板代码的时间压缩到了零——你的精力只需要放在"我想做什么"上。