04. 并发基础:goroutine、channel、context

2 阅读3分钟

04. 并发基础:goroutine、channel、context

Go 的并发模型是它非常有代表性的特性之一。你不需要一开始就写复杂并发程序,但必须先看懂常见的 goroutine、channel 和 context.Context

本节目标

  • 理解 goroutine 是什么
  • 掌握 channel 的基本通信方式
  • 认识 select 和超时控制
  • 理解为什么后端代码喜欢把 context.Context 作为第一个参数

goroutine:轻量级并发任务

Go 用 go 关键字启动一个并发任务:

go func() {
    fmt.Println("running in background")
}()

你可以把它理解成“把这段函数放到后台执行”,但它比传统线程更轻量。

为什么它在后端里很常见

因为后端经常会遇到这些场景:

  • 同时处理多个请求
  • 后台执行同步任务
  • 做超时控制或优雅关闭
  • 推送实时进度、流式响应

不过也要记住:不是所有逻辑都该起 goroutine。能同步完成的逻辑,先保持简单。

channel:goroutine 之间的通信管道

channel 用于在 goroutine 之间传递数据:

ch := make(chan string)

go func() {
    ch <- "hello"
}()

msg := <-ch
fmt.Println(msg)

这比共享可变状态更直观,也更符合 Go 的并发哲学。

带缓冲 channel

progressCh := make(chan int, 10)

progressCh <- 1
progressCh <- 2

带缓冲的 channel 在一定容量内发送时不会立刻阻塞,适合做进度上报、任务排队等场景。

定向 channel

在函数参数里,Go 允许你限制 channel 的方向:

func report(progressCh chan<- string) {
    progressCh <- "50%"
}

这里的 chan<- 表示“只发送,不接收”。这种写法在工程代码里很有用,因为它能明确表达函数职责。

select:监听多个并发事件

select {
case msg := <-ch:
    fmt.Println("收到消息:", msg)
case <-time.After(2 * time.Second):
    fmt.Println("超时")
}

select 常用于:

  • 超时控制
  • 同时等待多个 channel
  • 监听取消信号

context.Context:取消、超时和请求链路

Go 后端里大量函数的第一个参数都是 context.Context

func (r *RepoRepository) List(ctx context.Context) ([]Repository, error) {
    // ...
    return nil, nil
}

context 主要解决三件事:

  • 传递取消信号
  • 传递超时 / 截止时间
  • 贯穿一次请求的上下文信息

比如 HTTP 请求超时或用户中断时,底层数据库查询、外部 API 调用都可以跟着停止。

一个简化例子

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 一路传下去:

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)
}

而单分支同步又会接收一个进度通道:

func (s *SyncService) SyncSingleBranchCommits(
    ctx context.Context,
    client *gitea_client.Client,
    giteaRepoID int64,
    branchID int,
    progressCh chan<- helpers.ProgressMessage,
) (int, error) {
    // ...
}

从这个函数签名就能读出很多信息:

  • 这是一个可能耗时的任务
  • 它支持取消
  • 它会向外上报实时进度

并发编程的几个提醒

  • 不要为了“更快”盲目起 goroutine,先确认是否真的有并发价值
  • channel 很好用,但复杂状态同步时也可能需要 sync.Mutexsync.WaitGroup
  • 共享 map 时要小心数据竞争
  • context 要沿调用链传递,不要中途随意丢掉

小结

这一节的重点不是马上写复杂并发,而是先建立三个意识:

  1. goroutine 用来并发执行任务
  2. channel 用来安全地交换数据和信号
  3. context.Context 用来控制任务生命周期