04. 并发基础:goroutine、channel、context
Go 的并发模型是它非常有代表性的特性之一。你不需要一开始就写复杂并发程序,但必须先看懂常见的 goroutine、channel 和 context.Context。
本节目标
- 理解 goroutine 是什么
- 掌握 channel 的基本通信方式
- 认识
select和超时控制 - 理解为什么后端代码喜欢把
context.Context作为第一个参数
goroutine:轻量级并发任务
Go 用 go 关键字启动一个并发任务:
go
go func() {
fmt.Println("running in background")
}()
你可以把它理解成"把这段函数放到后台执行",但它比传统线程更轻量。
为什么它在后端里很常见
因为后端经常会遇到这些场景:
- 同时处理多个请求
- 后台执行同步任务
- 做超时控制或优雅关闭
- 推送实时进度、流式响应
不过也要记住:不是所有逻辑都该起 goroutine。能同步完成的逻辑,先保持简单。
channel:goroutine 之间的通信管道
channel 用于在 goroutine 之间传递数据:
go
ch := make(chan string)
go func() {
ch <- "hello"
}()
msg := <-ch
fmt.Println(msg)
这比共享可变状态更直观,也更符合 Go 的并发哲学。
带缓冲 channel
go
progressCh := make(chan int, 10)
progressCh <- 1
progressCh <- 2
带缓冲的 channel 在一定容量内发送时不会立刻阻塞,适合做进度上报、任务排队等场景。
定向 channel
在函数参数里,Go 允许你限制 channel 的方向:
go
func report(progressCh chan<- string) {
progressCh <- "50%"
}
这里的 chan<- 表示"只发送,不接收"。这种写法在工程代码里很有用,因为它能明确表达函数职责。
select:监听多个并发事件
css
select {
case msg := <-ch:
fmt.Println("收到消息:", msg)
case <-time.After(2 * time.Second):
fmt.Println("超时")
}
select 常用于:
- 超时控制
- 同时等待多个 channel
- 监听取消信号
context.Context:取消、超时和请求链路
Go 后端里大量函数的第一个参数都是 context.Context:
go
func (r *RepoRepository) List(ctx context.Context) ([]Repository, error) {
// ...
return nil, nil
}
context 主要解决三件事:
- 传递取消信号
- 传递超时 / 截止时间
- 贯穿一次请求的上下文信息
比如 HTTP 请求超时或用户中断时,底层数据库查询、外部 API 调用都可以跟着停止。
一个简化例子
go
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
done := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
done <- "同步完成"
}()
select {
case result := <-done:
fmt.Println(result)
case <-ctx.Done():
fmt.Println("任务超时:", ctx.Err())
}
在本项目里的实际意义
例如同步服务会把 context.Context 一路传下去:
go
func (s *SyncService) SyncBranches(ctx context.Context, client *gitea_client.Client, giteaRepoID int64) (int, error) {
repo, err := s.repoRepo.GetWithGiteaID(ctx, giteaRepoID)
if err != nil {
return 0, fmt.Errorf("仓库不存在: %w", err)
}
return s.SyncRepoBranches(ctx, client, repo)
}
而单分支同步又会接收一个进度通道:
go
func (s *SyncService) SyncSingleBranchCommits(
ctx context.Context,
client *gitea_client.Client,
giteaRepoID int64,
branchID int,
progressCh chan<- helpers.ProgressMessage,
) (int, error) {
// ...
}
从这个函数签名就能读出很多信息:
- 这是一个可能耗时的任务
- 它支持取消
- 它会向外上报实时进度
并发编程的几个提醒
- 不要为了"更快"盲目起 goroutine,先确认是否真的有并发价值
- channel 很好用,但复杂状态同步时也可能需要
sync.Mutex、sync.WaitGroup - 共享 map 时要小心数据竞争
- context 要沿调用链传递,不要中途随意丢掉
小结
这一节的重点不是马上写复杂并发,而是先建立三个意识:
- goroutine 用来并发执行任务
- channel 用来安全地交换数据和信号
context.Context用来控制任务生命周期