本文详解如何用 go 语言解析含 url 与间隔时间的配置文件,结合 time.ticker 和 goroutine 安全地发起定时 http 请求,并规避常见并发陷阱(如变量捕获、nil body 关闭)。
本文详解如何用 go 语言解析含 url 与间隔时间的配置文件,结合 time.ticker 和 goroutine 安全地发起定时 http 请求,并规避常见并发陷阱(如变量捕获、nil body 关闭)。
在构建服务健康监控系统时,需对多个目标站点进行周期性探测——既要测量响应延迟,也要验证 HTTP 状态码。Go 提供了简洁高效的并发原语,但初学者常因文件解析错误、goroutine 闭包变量捕获、未校验 resp 是否为 nil 等问题导致 panic。以下是一个健壮、可扩展的实现方案。
✅ 正确解析配置文件
你的 url_list.txt 应按行组织,每行包含一个 URL 和对应检测间隔(单位:秒),以制表符 \t 分隔:
http://google.com 5 http://nike.com 10
关键错误在于:你用 strings.Split(string(content), "\t") 直接按 tab 拆分整个文件内容,而忽略了换行符 \n。这会导致所有行被混在一起,lines[1] 可能是空字符串或非法数值,进而使 strconv.Atoi 失败,后续逻辑崩溃。
正确做法是先按行分割,再逐行按 tab 拆分:
content, err := ioutil.ReadFile("url_list.txt")
if err != nil {
log.Fatal("无法读取配置文件:", err)
}
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
var targets []struct {
URL string
Timeout time.Duration
}
for _, line := range lines {
if line == "" {
continue
}
parts := strings.Split(line, "\t")
if len(parts) < 2 {
log.Printf("跳过格式错误行: %q", line)
continue
}
url := strings.TrimSpace(parts[0])
durationSec, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
log.Printf("跳过无效时间值 %q: %v", parts[1], err)
continue
}
targets = append(targets, struct {
URL string
Timeout time.Duration
}{
URL: url,
Timeout: time.Second * time.Duration(durationSec),
})
}✅ 安全发起 HTTP 请求(含超时与错误处理)
原始代码中 defer resp.Body.Close() 在 resp 为 nil 时会 panic(http.Get 失败时返回 nil, error)。必须先判断 err,再操作 resp:
func getRespTime(url string, timeout time.Duration) {
start := time.Now()
log.Printf("→ 开始请求: %s (超时 %v)", url, timeout)
client := &http.Client{
Timeout: timeout,
}
resp, err := client.Get(url)
if err != nil {
elapsed := time.Since(start)
log.Printf("✗ %s 超时或连接失败 (%v): %v", url, elapsed, err)
return
}
defer resp.Body.Close() // 此时 resp 必不为 nil
elapsed := time.Since(start)
log.Printf("✓ %s 返回 %s (耗时 %v)", url, resp.Status, elapsed)
}⚠️ 注意:http.DefaultClient 默认无超时,务必显式设置 Timeout,否则单个卡死请求会阻塞整个 goroutine。
✅ 使用 Ticker + Worker Pool 实现定时并发调度
避免在 ticker 循环内直接启动大量 goroutine(易失控)。推荐模式:每个 URL 绑定独立 ticker,或统一调度 + 限流 worker pool。此处采用 每个目标独立 ticker,更清晰且天然隔离:
func main() {
// ... 上述解析 targets ...
var wg sync.WaitGroup
for _, t := range targets {
wg.Add(1)
go func(target struct {
URL string
Timeout time.Duration
}) {
defer wg.Done()
ticker := time.NewTicker(target.Timeout)
defer ticker.Stop()
for range ticker.C {
getRespTime(target.URL, target.Timeout)
}
}(t) // ✅ 关键:传值而非引用,避免闭包捕获循环变量
}
log.Println("✅ 启动", len(targets), "个定时探测任务,按 Ctrl+C 退出")
// 阻塞主 goroutine,等待信号终止
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
<-sig
log.Println("? 正在退出...")
}? 关键修复总结
- 文件解析:Split("\n") → Split("\t"),非一步到位;
- nil 安全:if err != nil { return } 后再访问 resp;
- 闭包陷阱:for range 中启动 goroutine 时,必须将 t 作为参数传入,否则所有 goroutine 共享最后一个 t 的值;
- 资源释放:使用 defer ticker.Stop() 避免内存泄漏;
- 可观测性:日志明确标注成功/失败、耗时、状态码,便于排查。
此方案支持任意数量 URL,各按自定义频率独立探测,线程安全、鲁棒性强,适合作为轻量级服务巡检工具的基础框架。

















