为什么 Go 一个 HTTP 服务可以同时处理数万连接
Why: 一个 Go 服务为什么可以同时处理几万甚至几十万连接?
答案并不复杂,Go net/http 的实现采用了每个 TCP 连接对应一个 Goroutine 的模型。
服务启动流程
一个 HTTP 服务通常从下面这行代码开始:
http.ListenAndServe(":8080", nil)
它内部的调用流程如下:

核心代码:
for {
rw, err := l.Accept()
if err != nil {
if s.shuttingDown() {
return ErrServerClosed
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
}
connCtx := ctx
if cc := s.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := s.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
流程非常简单:
- 监听端口。
- 不断调用
Accept()接收新的 TCP 连接。 - 每接收到一个连接,就启动一个新的 Goroutine 执行
conn.serve()。
因此,一个 TCP 连接对应一个 conn.serve Goroutine。
为什么可以支持数万连接?
假设:
- 有 10000 个客户端在线。
- 每个客户端保持一个 Keep-Alive 连接。
那么服务端大约会维护:
10000 TCP Connection
≈
10000 conn.serve Goroutine
Go 的 Goroutine 初始栈只有几 KB,并由运行时负责调度,因此创建数万个 Goroutine 的成本远低于传统线程模型。
这也是 Go 非常适合编写网络服务器的重要原因之一。
总结一下
在 http1.1 前提,开启 Keep-Alive 的情况下:
- 每一个tcp连接一个conn.serve goroutine
- 浏览器通常限制同域约6条TCP并发
- 服务端 goroutine 数量约等于所有客户端 TCP 连接总和,比如 1万用户 x 6 约等于 6 万TCP连接,约等于6万 groutine
keep-alive 价值
- 减少 TCP 三次握手
- 减少 TLS 握手(HTTPS)
- 减少 TIME_WAIT
- 减少 socket 创建关闭
- 减少 conn.serve goroutine 的创建销毁
- 提高连接复用率,降低延迟