今天用 OpenCode 写一个命令行死链检测器。输入一个网址,程序自动找出页面中的所有链接,并发检查每个链接是否可访问,最后按可用/失效分类展示结果。
这个工具的实用场景是网站维护——定期扫描自己的网站,发现失效的外链和内链。
npm install -g @anthropic/opencode 或参考 opencode.ai)安装好 OpenCode 后,在终端进入项目目录,运行 opencode 即可开始对话。
我对 OpenCode 说:
> 用 Go 写一个命令行死链检测工具。输入一个网址,提取页面所有链接,逐一检查可访问性,输出检测结果。要求代码约 80 行,只用标准库。
OpenCode 立刻生成了完整的 main.go 文件,包含:
flag 解析命令行参数(-url 和 -t 超时)<a href="..."> 中的链接net/http 发送 HEAD 请求检查每个链接要点: OpenCode 一次就生成了可编译运行的代码,无需手动写任何一行。关键是描述要清晰——说明输入、处理逻辑、输出格式。
单线程检测大量链接太慢,我继续对话:
> 改成并发检测,用 goroutine 和 channel 实现 worker pool 模式。加 -c 参数控制并发数,默认 10。
OpenCode 修改了代码:
urls channel 分发任务、results channel 收集结果sync.WaitGroup 确保所有 worker 完成后关闭 results channel-c 命令行参数要点: 并发编程通常需要手动写样板代码,OpenCode 直接生成了正确的 worker pool 模式,连 WaitGroup 和 channel 关闭时机都处理对了。
裸输出不够友好,我说:
> 输出分成两部分:先列出可用链接,再列出失效链接。失效链接前面显示 HTTP 状态码。
OpenCode 调整了 main 函数:
status == 200 分类到两个切片[状态码] URL要点: OpenCode 理解"分类展示"的意图,自动添加了切片分类、分组输出的逻辑。不需要解释"用 if 判断 200"这种实现细节。
我补了一句:
> 过滤掉空链接、# 锚点、javascript: 伪协议。
OpenCode 在链接提取函数里加了过滤条件:
if link == "" || strings.HasPrefix(link, "#") || strings.HasPrefix(link, "javascript:") {
continue
}
要点: 这种边界情况人类常忘,但对 OpenCode 随口提一句就能补上。这就是 AI 辅助开发的优势——把"别忘了"变成"加上了"。
package main
import (
"flag"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"sync"
"time"
)
type result struct {
url string
status int
}
func main() {
target := flag.String("url", "", "目标网址")
workers := flag.Int("c", 10, "并发数")
timeout := flag.Int("t", 5, "超时秒数")
flag.Parse()
if *target == "" {
fmt.Println("用法: deadlink -url https://example.com [-c 10] [-t 5]")
return
}
client := &http.Client{Timeout: time.Duration(*timeout) * time.Second}
resp, err := client.Get(*target)
if err != nil {
fmt.Printf("获取页面失败: %v\n", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
links := extractLinks(string(body))
fmt.Printf("从 %s 提取到 %d 个链接\n\n", *target, len(links))
urls := make(chan string, len(links))
results := make(chan result, len(links))
var wg sync.WaitGroup
for i := 0; i < *workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for u := range urls {
results <- result{u, checkLink(u, client)}
}
}()
}
for _, link := range links {
urls <- link
}
close(urls)
wg.Wait()
close(results)
var okLinks, brokenLinks []result
for r := range results {
if r.status == 200 {
okLinks = append(okLinks, r)
} else {
brokenLinks = append(brokenLinks, r)
}
}
fmt.Printf("✅ 可用链接 (%d):\n", len(okLinks))
for _, r := range okLinks {
fmt.Printf(" %s\n", r.url)
}
fmt.Printf("\n❌ 失效链接 (%d):\n", len(brokenLinks))
for _, r := range brokenLinks {
fmt.Printf(" [%d] %s\n", r.status, r.url)
}
}
func extractLinks(html string) []string {
re := regexp.MustCompile(`<a[^>]+href=["']([^"']+)["']`)
matches := re.FindAllStringSubmatch(html, -1)
seen := make(map[string]bool)
var links []string
for _, m := range matches {
link := strings.TrimSpace(m[1])
if link == "" || strings.HasPrefix(link, "#") || strings.HasPrefix(link, "javascript:") {
continue
}
if !seen[link] {
seen[link] = true
links = append(links, link)
}
}
return links
}
func checkLink(url string, client *http.Client) int {
resp, err := client.Head(url)
if err != nil {
return 0
}
defer resp.Body.Close()
return resp.StatusCode
}
从写出第一行到跑通测试,这篇文章中的每一步改动都只用了一句话描述需求,不用写一行 Go 代码。四轮对话加起来不到 4 分钟。
对比传统手写方式:写并发 worker pool 需要查文档、调 channel 关闭时机、处理 WaitGroup 计数,光这一步就得花不少时间。OpenCode 把"写代码"变成了"描述需求"——你负责定义 What,OpenCode 负责实现 How。