启动一个 Go 可执行文件会创建一个操作系统进程,并初始化 Go Runtime。Runtime 负责内存分配、垃圾回收和 Goroutine 调度,但它不会替程序员决定业务并发上限,也不会自动消除数据竞争。

GMP 模型是什么

1
2
3
G:Goroutine,等待执行的任务
M:Machine,操作系统线程
P:Processor,执行 Go 代码所需的调度资源

简化理解:

1
大量 G -> P 的本地运行队列 -> M 在线程上执行

GOMAXPROCS 主要控制同时执行 Go 代码的 P 数量,不等于 Goroutine 总数,也不等于数据库连接数。

函数不等于 Goroutine

普通调用会在当前 Goroutine 中同步执行:

1
user := loadUser(id)

加上 go 才会启动新的 Goroutine:

1
go loadUser(id)

第二种写法不会自动得到返回值,也不会自动等待完成。是否值得并发,取决于任务能否独立执行、是否存在等待、资源是否允许以及怎样收集错误。

Channel 如何传递结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
resultCh := make(chan User, 1)
errCh := make(chan error, 1)

go func() {
user, err := loadUser(id)
if err != nil {
errCh <- err
return
}
resultCh <- user
}()

select {
case user := <-resultCh:
fmt.Println(user.Name)
case err := <-errCh:
log.Println(err)
case <-time.After(2 * time.Second):
log.Println("timeout")
}

无缓冲 Channel 的发送要等接收方准备好;容量为 1 的缓冲 Channel 可以先存放一个值。Channel 适合传递数据、所有权或完成信号,不是“全局变量”或 Promise 的直接替代品。

Channel 还是 Mutex

场景 更自然的工具
任务流水线、结果传递、工作队列 Channel
多个 Goroutine 保护同一 Map 或计数器 sync.Mutex / sync.RWMutex
简单原子计数 sync/atomic
只执行一次初始化 sync.Once

共享数据如果被并发读写,必须建立同步关系。可以使用 go test -race ./... 检查常见数据竞争。

Context 负责取消链路

HTTP 请求结束后,相关数据库查询和下游调用也应停止:

1
2
3
4
5
6
7
8
9
func loadProfile(ctx context.Context, id int64) (Profile, error) {
row := db.QueryRowContext(ctx,
"SELECT id, name FROM users WHERE id = ?", id,
)

var profile Profile
err := row.Scan(&profile.ID, &profile.Name)
return profile, err
}

Context 用来传递取消、截止时间和请求范围值,不应把所有业务参数都塞进去。

并发必须有上限

一百个 Goroutine 同时查数据库,通常意味着一百次 SQL 尝试,而不是 Runtime 自动合并成一次。可以用连接池和信号量共同限制:

1
2
3
4
5
6
sem := make(chan struct{}, 20)

sem <- struct{}{}
defer func() { <-sem }()

return queryDatabase(ctx)

同时设置 HTTP、数据库和外部调用超时,并确保每条退出路径都会释放锁、连接、Ticker 和 Channel 等资源。

延伸阅读

站内搜索

没有找到内容!