Go网络编程本质是“同步阻塞API+goroutine调度+netpoll驱动”;Accept/Read/Write看似阻塞实为协程切换,需手动处理返回值、deadline重置、资源释放。

Go 的网络编程基础不是“异步非阻塞 IO 模型”,而是“同步阻塞 API + goroutine 调度 + netpoll 驱动”的组合。它表面写法像传统阻塞式(比如 conn.Read() 会挂起),但实际并发能力远超 Java BIO,关键在于 Go runtime 把系统级的阻塞调用(如 epoll_wait)封装成了对用户透明的协程调度点。
为什么 net.Listener.Accept() 看似阻塞却不卡主线程?
因为 Accept() 在底层触发的是系统调用(Linux 下是 accept4),但它被 Go runtime 的网络轮询器(netpoll)接管:当没有新连接时,当前 goroutine 被挂起、让出 M(OS 线程),而不是阻塞整个线程。等内核通知有连接就绪,runtime 自动唤醒该 goroutine —— 你写的 for { conn, _ := listener.Accept() } 看似循环阻塞,实则每个 Accept() 都是轻量级协程切换。
- 不要手动设
SO_REUSEPORT:Go 默认已做优化,多进程负载分担才需开启 - 务必配
listener.Close()+context.WithTimeout控制生命周期,否则程序退出时未处理完的连接可能丢包 - 错误日志里出现
use of closed network connection,大概率是listener.Close()被提前调用了
conn.Read() 和 conn.Write() 为什么不能当“一次搞定”用?
TCP 是字节流协议,Read() 最多返回当前内核接收缓冲区里有的数据,Write() 最多拷贝进 socket 发送缓冲区能容纳的字节数 —— 它们都可能少于你传入的切片长度。标准库不重试,也不聚合,你要自己处理 n 的情况。
- 直接
conn.Read(buf)就认为读完了整包 → 必然遇到粘包或半包 - 文本协议建议用
bufio.Reader+ReadString('\n')或ReadBytes('\n') - 二进制协议必须先解析包头长度字段,再循环
Read()直到收满指定字节数 - 每次调用
Read()前必须重新设conn.SetReadDeadline(),超时后不重设,下次读会立即返回 timeout 错误
HTTP handler 函数为啥必须同步写,却又能“异步干活”?
http.Server 内部为每个请求启一个 goroutine,并把该 goroutine 绑定到 http.ResponseWriter 的整个生命周期。你写的 func(w http.ResponseWriter, r *http.Request) 是同步执行的,但可以安全地在其中启动新 goroutine 做耗时操作 —— 前提是不能在那个新 goroutine 里碰 w 或 r.Body。
立即学习“go语言免费学习笔记(深入)”;
- 错:
go func() { w.Write([]byte("done")) }()→ panic: write on closed response body - 对:用
sync.WaitGroup或 channel 等待异步任务完成,再统一写w -
r.Body是io.ReadCloser,必须显式r.Body.Close(),否则 HTTP/1.1 keep-alive 连接无法复用
最常被忽略的一点:Go 的“高并发”不来自魔法,而来自你是否真正理解 Read()/Write() 的返回值语义、deadline 的重置规则、以及 goroutine 生命周期和资源释放的耦合关系。写十行代码容易,让它们在百万连接下不泄漏、不 panic、不丢数据,才是基础里的硬核部分。











