
本文介绍在 go 开发中如何通过信号捕获机制实现 tcp 服务器的优雅关闭,避免端口被占用、资源泄漏等问题,尤其适用于本地调试场景。
在 Go 编写网络服务(如聊天服务器)时,频繁重启服务是常态。若仅依赖 Ctrl+C 终止进程,Go 运行时会立即终止程序,导致 defer ln.Close() 等清理逻辑无法执行——监听套接字未释放、连接未主动关闭、端口处于 TIME_WAIT 或残留状态,从而引发“address already in use”错误(尤其在 Windows/Cygwin 下更明显,表现为任务管理器中残留多个 server.exe 进程)。
真正的解决方案不是回避 Ctrl+C,而是主动捕获并响应它:使用 os/signal 包监听 SIGINT(即 Ctrl+C 发送的信号),在收到信号后执行有序关闭流程。
✅ 正确做法:信号驱动的优雅关闭
以下是一个完整、可直接复用的服务端模板,包含监听启动、信号等待与资源释放:
package main
import (
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// 1. 启动 TCP 监听
ln, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal("Failed to start server:", err)
}
defer ln.Close() // 防御性 defer(虽非总触发,但无害)
log.Println("Server started on :8080. Press Ctrl+C to shut down gracefully.")
// 2. 启动服务 goroutine(模拟你的主逻辑)
go func() {
for {
conn, err := ln.Accept()
if err != nil {
// 监听被关闭时 Accept 会返回 error,需检查是否因 Close() 引起
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
log.Printf("Temporary accept error: %v", err)
continue
}
log.Printf("Accept error: %v", err)
return
}
log.Printf("New connection from %s", conn.RemoteAddr())
// 示例:简单 echo 处理(实际应启动新 goroutine 并设超时)
go handleConnection(conn)
}
}()
// 3. 等待 SIGINT(Ctrl+C)信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) // 同时支持 Ctrl+C 和 kill 命令
// 4. 阻塞等待信号,收到后执行清理
<-sigChan
log.Println("Received shutdown signal. Closing listener...")
// 5. 主动关闭 listener —— 这将使 Accept() 返回 error,触发服务 goroutine 退出
if err := ln.Close(); err != nil {
log.Printf("Error closing listener: %v", err)
}
// 可选:等待活跃连接完成(根据业务需求添加超时与等待逻辑)
log.Println("Server stopped gracefully.")
}
func handleConnection(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
conn.Write(buf[:n])
}⚠️ 关键注意事项
- defer ln.Close() 单独无效:defer 仅在函数自然返回时执行;Ctrl+C 触发的是进程强制终止,不经过 defer 流程。
- signal.Notify 必须早于 :否则可能丢失信号(尤其是快速按 Ctrl+C 时)。
- ln.Close() 是核心动作:调用它会使后续 ln.Accept() 立即返回 error,从而让服务 goroutine 安全退出,这是优雅关闭的基石。
- Windows/Cygwin 兼容性:上述代码在 Windows(含 Cygwin/WSL)和类 Unix 系统均有效;syscall.SIGINT 在 Windows 上对应 CTRL_C_EVENT。
-
生产增强建议:
- 添加 context.WithTimeout 控制连接处理超时;
- 使用 sync.WaitGroup 等待所有活跃连接 goroutine 结束;
- 记录关闭前的统计信息(如已服务请求数);
- 考虑 syscall.SIGQUIT 或自定义 HTTP /shutdown 端点用于远程触发。
掌握信号处理机制,不仅能解决端口复用问题,更是构建高可靠性 Go 服务的必备基础能力。从现在开始,告别 kill -9 和手动改端口,让每一次重启都干净、可控、可预期。










