
本文介绍如何使用 Go 定期检查指定端口的 TCP 连接状态,重点解决因误传 sync.WaitGroup 值类型导致的 goroutine 永久阻塞问题,并提供健壮、可维护的监控实现方案。
本文介绍如何使用 go 定期检查指定端口的 tcp 连接状态,重点解决因误传 `sync.waitgroup` 值类型导致的 goroutine 永久阻塞问题,并提供健壮、可维护的监控实现方案。
在 Go 中实现对 TCP 端口连接数的周期性监控(如每 5 秒执行一次 lsof -i:<port></port>)是一个常见运维需求,但初学者容易陷入几个关键陷阱:sync.WaitGroup 误传值类型、 ticker 生命周期管理不当、命令执行错误处理缺失、以及解析逻辑脆弱。下面我们将逐一剖析并给出生产就绪的解决方案。
? 核心问题修复:WaitGroup 必须传指针
原代码中 count(port, wg) 将 sync.WaitGroup 按值传递,导致每个 goroutine 操作的是 wg 的副本,wg.Done() 对主 goroutine 中的原始实例毫无影响,最终 wg.Wait() 永远阻塞——这正是 ticker “只触发一次” 的根本原因。正确做法是*始终传递 `sync.WaitGroup`**:
func connectionsCount() {
var wg sync.WaitGroup // 声明普通变量
wg.Add(len(ports))
for _, port := range ports {
go count(port, &wg) // ✅ 传地址
}
wg.Wait() // 阻塞直到所有子 goroutine 调用 wg.Done()
}同时更新 count 函数签名:
func count(port int, wg *sync.WaitGroup) {
defer wg.Done() // ✅ 确保计数器递减
// ... 其余逻辑
}⚙️ 完整可运行示例(含健壮性增强)
以下为优化后的完整程序,已修复阻塞问题,并增强容错性与可观测性:
package main
import (
"fmt"
"log"
"os/exec"
"strings"
"sync"
"time"
)
type LsofEntry struct {
Command string
Pid string
User string
FD string
Type string
Device string
SizeOff string
Node string
Name string
TCPState string
}
var ports = []int{30001, 30002}
func main() {
ticker := time.NewTicker(5 * time.Second) // ⚠️ 避免过短间隔(如 5ms),防止系统过载
defer ticker.Stop()
// 使用 done channel 控制生命周期,避免 main 提前退出
done := make(chan struct{})
go func() {
for {
select {
case t := <-ticker.C:
fmt.Printf("? Tick at %s\n", t.Format(time.DateTime))
connectionsCount()
case <-done:
return
}
}
}()
// 模拟运行 60 秒后优雅退出(生产环境建议用 signal.Notify)
time.AfterFunc(60*time.Second, func() {
fmt.Println("✅ Monitoring completed. Shutting down...")
close(done)
})
// 阻塞等待,让 goroutine 有时间执行
select {}
}
func connectionsCount() {
var wg sync.WaitGroup
wg.Add(len(ports))
for _, port := range ports {
go count(port, &wg)
}
wg.Wait()
}
func count(port int, wg *sync.WaitGroup) {
defer wg.Done()
cmd := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port))
out, err := cmd.Output()
if err != nil {
// 注意:lsof 无匹配结果时返回非零退出码,但非真正错误
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
fmt.Printf("Port=%d → No active connections\n", port)
return
}
log.Printf("⚠️ lsof failed for port %d: %v", port, err)
return
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) <= 1 {
fmt.Printf("Port=%d → No output from lsof\n", port)
return
}
tcpStates := make(map[string]int)
for i, line := range lines {
if i == 0 { // 跳过表头
continue
}
fields := strings.Fields(line) // ✅ 更安全:strings.Fields 自动跳过空格/制表符
if len(fields) < 9 {
continue
}
// 字段索引参考 lsof -i 输出:COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nameField := fields[8]
if strings.HasPrefix(nameField, "TCP") {
// 解析 TCP 状态,如 "127.0.0.1:30001->127.0.0.1:54322 (ESTABLISHED)"
if idx := strings.LastIndex(nameField, "("); idx != -1 && strings.HasSuffix(nameField, ")") {
state := nameField[idx+1 : len(nameField)-1]
tcpStates[state]++
}
}
}
fmt.Printf("Port=%d → ", port)
for state, cnt := range tcpStates {
fmt.Printf("(TCP %s: %d) ", state, cnt)
}
fmt.Println()
}✅ 关键改进点总结
| 问题 | 修复方式 | 说明 |
|---|---|---|
WaitGroup 阻塞 |
传 &wg 指针,函数接收 *sync.WaitGroup
|
值传递导致 Done() 失效,goroutine 永不结束 |
| Ticker 未持续运行 | 移除 死锁,改用 <code>select {} + time.AfterFunc 或信号控制
|
原 无 goroutine 写入,main 卡死
|
lsof 错误处理脆弱 |
区分 ExitError 码 1(无连接)与真实错误 |
避免将“无连接”误判为崩溃 |
| 输出解析不稳定 | 改用 strings.Fields() 替代手动 Split(" ") + TrimSpace 循环 |
自动处理多空格、制表符,更鲁棒 |
| 资源滥用风险 | 将 ticker 间隔从 5ms 改为 5s,添加 defer ticker.Stop()
|
防止高频调用压垮系统或被限流 |
? 生产环境建议
-
替代方案优先级:
lsof是外部命令,依赖系统工具且性能开销大。长期项目推荐直接读取/proc/net/tcp(Linux)或使用netstat的 Go 原生库(如github.com/shirou/gopsutil)。 -
权限注意:
lsof查看其他用户进程需sudo,监控服务应以最小权限运行。 -
可观测性:将统计结果推送至 Prometheus(暴露为 Gauge)、写入日志或发送告警,而非仅
fmt.Println。 -
超时控制:为
exec.Command添加context.WithTimeout,防止单次lsof卡死。
通过以上修正,你的 Go 程序即可稳定、高效地完成端口连接数的周期性监控任务。

















