本文介绍如何使用 go 语言解析带间隔配置的 url 列表,通过 ticker 定时发起 http 请求,并安全并发地测量响应时间与状态码,避免常见闭包捕获、nil 指针和资源泄漏问题。
在服务健康监测场景里,定时轮询多个目标站点并统计响应性能(耗时、状态码)是个典型需求。Go 的 goroutine 和 net/http、time/ticker 原生支持让这类任务变得很顺手,但初学者很容易踩坑:文件解析出错、goroutine 闭包变量捕获、响应体没关闭导致 panic 或逻辑混乱。下面直接拆解关键步骤,把坑填平。
✅ 正确解析 URL 配置文件
原始代码里直接用 strings.Split(string(content), "t") 按 Tab 分割整个文件内容,换行符结构被忽略,结果 lines 数组变成类似 [url1, "5\nhttps://nike.com", "10"] 的畸形切片——后面 lines[i] 访问越界或解析失败也就不奇怪了。
正确做法是先按行分割(\n),再对每行按 Tab 分割(\t),同时过滤空行:
content, err := os.ReadFile("url_list.txt")
if err != nil {
log.Fatal("读取文件失败:", err)
}
lines := strings.Fields(string(content)) // 或 strings.Split(strings.TrimSpace(string(content)), "\n")
urls := make([]struct{ URL string; Interval int }, 0)
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.Split(line, "\t")
if len(parts) < 2 {
log.Printf("跳过格式错误行: %q", line)
continue
}
url := strings.TrimSpace(parts[0])
intervalSec, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
log.Printf("跳过无效间隔值 %q: %v", parts[1], err)
continue
}
urls = append(urls, struct{ URL string; Interval int }{URL: url, Interval: intervalSec})
}
⚠️ 注意:确保 .txt 文件中确实使用 Tab 字符(\t) 分隔 URL 与秒数,而非空格(可用 cat -A url_list.txt 验证)。若用空格,请改用 strings.Fields()。
✅ 安全并发请求与资源管理
原代码有两大隐患:
defer resp.Body.Close()写在 goroutine 内部但 resp 可能为 nil(请求失败时),直接 panic;for i := 0; i < len(lines)-1; i++循环中启动 goroutine,但 i 是外部变量,所有 goroutine 共享同一个 i(闭包陷阱),最终都访问lines[len(lines)-1]导致越界。
修正方案:显式传参 + 错误检查 + 确保 Close 只在 resp 非 nil 时调用:
func getRespTime(url string) {
start := time.Now()
fmt.Printf("[START] %s at %s\n", url, start.Format(time.TimeOnly))
resp, err := http.Get(url)
if err != nil {
dur := time.Since(start)
fmt.Printf("[FAIL] %s | %v | %.3fs\n", url, err, dur.Seconds())
return // 不调用 Close,resp 为 nil
}
defer resp.Body.Close() // 此处 resp 必非 nil
dur := time.Since(start)
fmt.Printf("[OK] %s | %s | %.3fs\n", url, resp.Status, dur.Seconds())
}
✅ 构建健壮的定时轮询主逻辑
不再依赖全局 ticker 和复杂 channel 控制,改为每个 URL 启动独立 ticker——更灵活,维护起来也清爽:
func main() {
urls := parseURLs("url_list.txt") // 上述解析函数
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go func(u struct{ URL string; Interval int }) {
defer wg.Done()
ticker := time.NewTicker(time.Duration(u.Interval) * time.Second)
defer ticker.Stop()
for range ticker.C {
getRespTime(u.URL)
}
}(u) // 立即传值,避免闭包捕获
}
// 模拟运行 60 秒后退出(生产环境建议用信号控制)
time.Sleep(60 * time.Second)
fmt.Println("监控结束。")
}
? 补充建议与最佳实践
- 超时控制:http.Get 默认无超时,建议用 http.Client 配置 Timeout,防止单个请求阻塞:
client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Get(url) - 重试机制:对临时性失败(5xx、连接超时)可加上指数退避重试。
- 结果聚合:将每次结果写入日志文件或推向 Prometheus/Grafana。
- 并发限制:若 URL 数量极大,可以引入 semaphore 或 worker pool 控制并发请求数,避免端口耗尽。
经过以上重构,监控程序能稳定运行、准确计时、清晰输出,可扩展性和可维护性也有保证。
