启动一个 Go 可执行文件会创建一个操作系统进程,并初始化 Go Runtime。Runtime 负责内存分配、垃圾回收和 Goroutine 调度,但它不会替程序员决定业务并发上限,也不会自动消除数据竞争。
GMP 模型是什么
1 | G:Goroutine,等待执行的任务 |
简化理解:
1 | 大量 G -> P 的本地运行队列 -> M 在线程上执行 |
GOMAXPROCS 主要控制同时执行 Go 代码的 P 数量,不等于 Goroutine 总数,也不等于数据库连接数。
函数不等于 Goroutine
普通调用会在当前 Goroutine 中同步执行:
1 | user := loadUser(id) |
加上 go 才会启动新的 Goroutine:
1 | go loadUser(id) |
第二种写法不会自动得到返回值,也不会自动等待完成。是否值得并发,取决于任务能否独立执行、是否存在等待、资源是否允许以及怎样收集错误。
Channel 如何传递结果
1 | resultCh := make(chan User, 1) |
无缓冲 Channel 的发送要等接收方准备好;容量为 1 的缓冲 Channel 可以先存放一个值。Channel 适合传递数据、所有权或完成信号,不是“全局变量”或 Promise 的直接替代品。
Channel 还是 Mutex
| 场景 | 更自然的工具 |
|---|---|
| 任务流水线、结果传递、工作队列 | Channel |
| 多个 Goroutine 保护同一 Map 或计数器 | sync.Mutex / sync.RWMutex |
| 简单原子计数 | sync/atomic |
| 只执行一次初始化 | sync.Once |
共享数据如果被并发读写,必须建立同步关系。可以使用 go test -race ./... 检查常见数据竞争。
Context 负责取消链路
HTTP 请求结束后,相关数据库查询和下游调用也应停止:
1 | func loadProfile(ctx context.Context, id int64) (Profile, error) { |
Context 用来传递取消、截止时间和请求范围值,不应把所有业务参数都塞进去。
并发必须有上限
一百个 Goroutine 同时查数据库,通常意味着一百次 SQL 尝试,而不是 Runtime 自动合并成一次。可以用连接池和信号量共同限制:
1 | sem := make(chan struct{}, 20) |
同时设置 HTTP、数据库和外部调用超时,并确保每条退出路径都会释放锁、连接、Ticker 和 Channel 等资源。