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

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.Mutexsync.WaitGroup
  • 共享 map 时要小心数据竞争
  • context 要沿调用链传递,不要中途随意丢掉

小结

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

  1. goroutine 用来并发执行任务
  2. channel 用来安全地交换数据和信号
  3. context.Context 用来控制任务生命周期
相关推荐
审判长烧鸡3 小时前
GO裸奔【1】动态SQL
go·动态sql·切片
审判长烧鸡13 小时前
GO时区【2】跨时区应用
go·存储·时区
审判长烧鸡14 小时前
Go结构体与指针【2】接收者应该怎么用
go·指针·结构体·接收者
王中阳Go17 小时前
2026年了,还在纠结后端转AI要不要死磕Python?试试Go吧
后端·go·ai编程
审判长烧鸡1 天前
GO结构体与指针【1】什么时候用指针
go·指针·结构体
审判长烧鸡1 天前
GO错误处理【2】os.Exit(1)/panic/返回err的应用场景
go·异常处理·panic
审判长烧鸡1 天前
GO时区【4】PostgreSQL时区
postgresql·go
审判长烧鸡1 天前
GO时区【3】字段与连接设置
postgresql·go
审判长烧鸡1 天前
GO错误处理【1】不用try-catch用什么?
go·异常处理·try-catch·panic·fatal·os.exit
Go_error3 天前
Go database/sql 基于临时 channel 传递连接
后端·go