Go中限制TCP并发连接数的唯一可靠位置是在Accept()成功后、交付handler前,使用semaphore或带缓冲channel进行准入控制。

listen 时用 net.Listen 配合 net.ListenConfig 控制底层连接队列
Go 默认的 net.Listen("tcp", addr) 不限制已完成三次握手的连接排队数(即 backlog),但操作系统内核会截断,超出部分直接丢弃 SYN 包,客户端表现为“连接超时”或“connection refused”,而非服务端可控的拒绝逻辑。
真正能干预的点是:在调用 net.Listen 前,通过 net.ListenConfig 设置 KeepAlive 和控制 Control 函数拦截 socket 创建,不过它不直接提供并发数限制——那是应用层该做的事。所以别指望靠 listen 参数拦住所有连接。
- 想让内核排队更可控?设
net.ListenConfig{Control: func(fd uintptr) { syscall.SetsockoptInt32(int(fd), syscall.SOL_SOCKET, syscall.SO_BACKLOG, 128) }}(注意:仅 Linux 有效,且值受系统/proc/sys/net/core/somaxconn限制) - SO_BACKLOG 设太大没用:Go 的
accept循环如果太慢,队列积压照样丢包 - 这个参数只影响“已建立但还没被
Accept()拿走”的连接数,不是总并发连接数
用 semaphore 或带缓冲 channel 在 Accept 后做准入控制
最大并发连接数必须在连接被 Accept() 成功后、交付给 handler 前判定。这是唯一可靠的位置:此时连接已建立、FD 已分配、资源可计数。
推荐用 golang.org/x/sync/semaphore(比裸 channel 更易管理取消和超时):
立即学习“go语言免费学习笔记(深入)”;
var sem = semaphore.NewWeighted(int64(maxConns))
ln, _ := net.Listen("tcp", ":8080")
for {
conn, err := ln.Accept()
if err != nil {
continue
}
if !sem.TryAcquire(1) {
conn.Close() // 拒绝新连接,主动关掉
continue
}
go func(c net.Conn) {
defer sem.Release(1)
handleConn(c)
}(conn)
}
- 别用
sem.Acquire(ctx, 1)等待——连接已建好,等待等于占用 FD 资源,可能触发系统级 limit(如 ulimit -n) - 一定要在 goroutine 里调用
Release,且确保无论 panic 还是正常退出都释放,建议用defer - 权重设为 1 即可;若需按连接带宽/内存预估加权,再调整,但复杂度陡增
http.Server 的 MaxConns 和 MaxIdleConns 容易混淆
标准库 http.Server 没有 MaxConns 字段——这是常见误解。它只有 MaxIdleConns 和 MaxIdleConnsPerHost,这两个只管空闲连接复用,对新建连接毫无约束力。
如果你用的是 http.Serve,真正的并发控制仍得回到上一步:包装 net.Listener 或在 Server.Serve 前加闸。
- 错误做法:
srv := &http.Server{MaxIdleConns: 100}; srv.Serve(ln)—— 这完全不限制同时处理的请求数 - 正确做法:写个 wrapper listener,
Accept()时查 semaphore,再把 conn 交给http.Serve - 第三方库如
gorilla/handlers也不提供连接数硬限,它们侧重中间件链路,非连接生命周期
别漏掉文件描述符(FD)耗尽这个底层瓶颈
即使 Go 层用信号量卡住了连接数,如果进程打开的 FD 总数超限(ulimit -n),Accept() 会直接返回 "too many open files" 错误,程序崩溃或静默失败。
- 启动前检查:
ulimit -n,生产环境建议设到 65536 或更高 - 每个 TCP 连接至少占 1 个 FD,加上日志文件、数据库连接、临时文件等,实际可用数远小于 ulimit 值
- 用
lsof -p $(pidof yourapp) | wc -l实时观察 FD 使用量 - Go 1.21+ 可启用
runtime/debug.SetMemoryLimit辅助控制,但它不解决 FD 问题
真正难的不是写几行限流代码,而是确认你控制的到底是哪一层:内核队列、Go 的 goroutine 数、HTTP 的活跃请求,还是系统的 FD 总量。它们互相耦合,改一处可能暴露另一处短板。










