net.conn 不能直接存 map 因并发读写 panic;应使用 sync.rwmutex 保护 uint64 为 key 的 map,connmeta 内嵌 sync.once 和 atomic.bool 防 double-close。

为什么 net.Conn 不能直接存 map 里做长连接管理
因为并发读写 map 会 panic:fatal error: concurrent map read and map write。哪怕你只用 sync.Map,它也不适合高频更新的连接生命周期管理——比如心跳超时、主动断连、重连复用等场景下,需要原子性地删键 + 关闭连接 + 清理资源,sync.Map 的 LoadAndDelete 不保证回调执行顺序,容易漏关连接或 double-close。
实操建议:
- 用
map[uint64]*ConnMeta配合sync.RWMutex,读多写少时读锁开销极低 -
ConnMeta必须内嵌sync.Once和atomic.Bool控制关闭状态,避免 goroutine 反复 close 同一个net.Conn - 不要在
conn.Read()的 goroutine 里直接delete(m, id),应发信号给统一的清理协程
如何让 gorilla/websocket 支持百万级连接而不 OOM
默认配置下,每个 WebSocket 连接会分配 4KB+ 的读写缓冲区,百万连接就是 4GB+ 内存,还没算 goroutine 栈和连接元数据。更致命的是,gorilla/websocket 的 SetReadDeadline 在高并发心跳场景下会触发大量定时器,导致 runtime.timer 占用飙升。
实操建议:
- 用
websocket.Upgrader.CheckOrigin = func(r *http.Request) bool { return true }省掉 Origin 检查开销(内网可信场景) - 设置
websocket.Upgrader.ReadBufferSize = 1024和WriteBufferSize = 512,够用且可控 - 禁用默认心跳:
conn.SetPongHandler(nil),自己用time.Ticker批量轮询连接状态,避免每连接一个 timer - 用
runtime/debug.SetGCPercent(20)降低 GC 压力,配合pprof观察heap_inuse趋势
epoll / kqueue 在 Go 里是自动启用的吗
是,但仅限于 Go 1.19+ 默认开启的 netpoll 机制。不过它不等于直接调用系统 epoll_wait——Go runtime 封装了一层事件循环,对用户透明。问题在于:如果你用 conn.SetDeadline() 太频繁,会退化成每个连接绑一个 timer,失去事件驱动优势。
实操建议:
- 所有超时控制统一走一个
time.AfterFunc或timer.Reset()复用,不要每个读/写都设 deadline - 确认你的 Go 版本 ≥ 1.19,检查
GODEBUG=netdns=go+1是否误启用了阻塞 DNS 解析(影响 accept 性能) - 用
lsof -p $(pidof yourapp) | wc -l监控文件描述符数,超过 65535 时需提前ulimit -n 1048576
连接 ID 用 int64 还是 string 做 key
用 uint64。string 是不可变对象,每次拼接或哈希都要新分配内存;而 uint64 是值类型,map 查找快 3–5 倍,GC 压力几乎为零。更重要的是:客户端重连时,如果用 token 字符串做 key,无法快速识别“旧连接是否还在”,必须遍历 map;而用自增 uint64 + 时间戳高位,可天然支持连接时效判断。
实操建议:
- 生成 ID 用
atomic.AddUint64(&idGen, 1),不要用rand.Uint64()——后者碰撞概率在百万级不可忽略 - ID 高 32 位存启动时间戳(秒),低 32 位自增,便于从 ID 推出连接大致生命周期
- 绝不把 client IP:port 当作唯一标识,NAT、代理、短连接复用都会让它失效
/debug/pprof/goroutine?debug=1 这种全量堆栈 dump ——它们本身就会触发全局 stop-the-world。别等压测崩了才想起来关掉。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











