本文介绍如何使用 go 语言读取含 url 和间隔时间的配置文件,通过 ticker 定期发起 http 请求并统计响应耗时与状态码,同时规避 goroutine 闭包陷阱和文件解析错误。
本文介绍如何使用 go 语言读取含 url 和间隔时间的配置文件,通过 ticker 定期发起 http 请求并统计响应耗时与状态码,同时规避 goroutine 闭包陷阱和文件解析错误。
在 Go 中构建一个轻量级 HTTP 健康检查工具,关键在于:正确解析配置、安全并发执行请求、避免变量捕获错误、合理管理资源与超时。原代码存在多个典型问题:
- ❌ 文件按 一次性分割,导致 URL 和时间混杂(如 "http://google.com 5 http://nike.com 10" 分割后为 ["http://google.com 5 http://nike.com 10"],而非预期的多行);
- ❌ for i := 0; i < len(lines)-1; i++ 循环中启动 goroutine 时未捕获 lines[i] 的当前值,造成所有 goroutine 共享最终 i 值(经典的闭包变量捕获问题);
- ❌ defer resp.Body.Close() 写在 if err != nil 分支外但未加 resp != nil 判断,当请求失败时 resp 为 nil,触发 panic;
- ❌ in channel 被声明却未被任何 goroutine 消费,且 wg.Wait() 在无限 ticker 循环内永不执行,逻辑阻塞。
以下是修复后的完整可运行示例:
package main
import (
"bufio"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
)
// URLWithInterval 表示一条带轮询间隔(秒)的 URL 记录
type URLWithInterval struct {
URL string
Interval time.Duration
}
// parseURLFile 解析形如 "https://example.com 30" 的文本文件,每行一条记录
func parseURLFile(filename string) ([]URLWithInterval, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("failed to open %s: %w", filename, err)
}
defer file.Close()
var urls []URLWithInterval
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue // 跳过空行和注释
}
parts := strings.Split(line, " ")
if len(parts) < 2 {
return nil, fmt.Errorf("invalid format in line: %q (expected 'URL\tseconds')", line)
}
url := strings.TrimSpace(parts[0])
secondsStr := strings.TrimSpace(parts[1])
seconds, err := strconv.ParseInt(secondsStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid interval in line %q: %w", line, err)
}
urls = append(urls, URLWithInterval{
URL: url,
Interval: time.Second * time.Duration(seconds),
})
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading file: %w", err)
}
return urls, nil
}
// checkURL 发起单次 HTTP GET 请求,返回耗时与状态码(或错误)
func checkURL(url string) (time.Duration, string, error) {
start := time.Now()
client := &http.Client{
Timeout: 10 * time.Second, // 防止请求永久挂起
}
resp, err := client.Get(url)
if err != nil {
return time.Since(start), "", err
}
defer resp.Body.Close() // 此处 resp 不为 nil,安全调用
return time.Since(start), resp.Status, nil
}
// monitorURL 按指定间隔持续监控单个 URL
func monitorURL(u URLWithInterval, wg *sync.WaitGroup) {
defer wg.Done()
ticker := time.NewTicker(u.Interval)
defer ticker.Stop()
fmt.Printf("[START] Monitoring %s every %v
", u.URL, u.Interval)
for range ticker.C {
duration, status, err := checkURL(u.URL)
if err != nil {
log.Printf("[ERROR] %s → %v (took %v)", u.URL, err, duration)
} else {
fmt.Printf("[OK] %s → %s (took %v)
", u.URL, status, duration)
}
}
}
func main() {
urls, err := parseURLFile("url_list.txt")
if err != nil {
log.Fatal("Failed to parse URL file:", err)
}
if len(urls) == 0 {
log.Fatal("No valid URLs found in file")
}
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go monitorURL(u, &wg)
}
// 主 goroutine 保持运行(可按 Ctrl+C 退出)
fmt.Println("HTTP monitor started. Press Ctrl+C to exit.")
select {} // 阻塞等待信号
}? 关键改进说明:
- ✅ 健壮文件解析:使用 bufio.Scanner 按行读取,再对每行 strings.Split(..., " "),明确区分 URL 与间隔;支持跳过空行/注释;
- ✅ goroutine 安全:for _, u := range urls 中直接传值 u 到 goroutine,避免闭包捕获循环变量;
- ✅ HTTP 安全调用:checkURL 函数内统一处理 err 与 resp,仅在 resp != nil 时调用 Close();显式设置 http.Client.Timeout 防止 hang;
- ✅ 职责分离:monitorURL 封装单 URL 的长期监控逻辑,主函数专注调度与初始化;
- ✅ 可观测性:清晰日志格式,包含时间戳、状态、耗时,便于运维排查。
⚠️ 注意事项:
- 确保 url_list.txt 使用真实 Tab 字符分隔(非空格),可用 cat -A url_list.txt 验证;
- 生产环境建议增加重试机制、结果聚合(如 Prometheus 指标暴露)、失败告警;
- 若需并发控制(如限制最大并发请求数),可引入 semaphore 或 worker pool 模式,而非无限制启动 goroutine。
该方案简洁、可靠、符合 Go 最佳实践,适用于服务健康巡检、SLA 监控等场景。

















