Go 语言入门系列(五):并发编程——goroutine、channel 与同步

本篇目标:掌握 Go 最被称道的语言级并发能力。核心哲学一句话:不要通过共享内存来通信,而要通过通信来共享内存。

一、goroutine

在函数调用前加 go,就在新的 goroutine 中运行。它是运行时管理的轻量级线程,创建成本极低,开几万个也没问题:

func say(s string) {
    for i := 0; i < 3; i++ {
        fmt.Println(s)
        time.Sleep(100 * time.Millisecond)
    }
}

go say("world")   // 新 goroutine
say("hello")      // 当前 goroutine

注意:main 返回时所有 goroutine 立即终止,不会等它们跑完。所以上面的例子可能看不到 "world" 全部输出。

二、channel

channel 是 goroutine 之间通信的管道:

ch := make(chan string)

go func() {
    ch <- "来自 goroutine 的消息"    // 发送
}()

msg := <-ch                          // 接收,阻塞等待

无缓冲 vs 有缓冲:

  • 无缓冲(make(chan int)):发送和接收同步,发送方阻塞直到有人接收
  • 有缓冲(make(chan int, 10)):缓冲区未满时发送不阻塞

关闭与遍历:

close(ch)                 // 由发送方关闭,接收方不关

for v := range ch {       // 通道关闭后自动结束循环
    fmt.Println(v)
}

v, ok := <-ch             // ok 为 false 表示通道已关闭且无数据

三、select 多路复用

select 同时等待多个 channel,哪个就绪走哪个:

select {
case msg := <-ch1:
    fmt.Println("收到:", msg)
case ch2 <- 42:
    fmt.Println("发送成功")
case <-time.After(time.Second):
    fmt.Println("超时")
default:
    fmt.Println("都没就绪,不阻塞")
}

time.After 做超时,default 实现非阻塞尝试。这是并发控制的常用组合。

四、sync 包

channel 适合传数据,但有时只需同步状态,sync 更直接:

WaitGroup——等一组 goroutine 全部完成:

var wg sync.WaitGroup

for i := 0; i < 3; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        fmt.Println(n)
    }(i)
}

wg.Wait()   // 阻塞直到计数归零

Mutex——保护共享数据:

var (
    mu  sync.Mutex
    sum int
)

mu.Lock()
sum++
mu.Unlock()

Once——保证只执行一次,适合初始化:

var once sync.Once
once.Do(func() { /* 只执行一次 */ })

五、context:超时、取消与传值

context 是控制 goroutine 生命周期的标准方式,尤其适合请求链路:

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

select {
case result := <-doWork(ctx):
    fmt.Println(result)
case <-ctx.Done():
    fmt.Println("超时或被取消:", ctx.Err())
}

WithCancel 手动取消,WithTimeout 超时取消,WithValue 传请求级数据(只放必要的元数据,别滥用)。

六、常见并发陷阱

1. 循环变量捕获——旧版 Go 中,goroutine 里直接用循环变量会全部拿到最后一个值。解决:作为参数传入。

for i := 0; i < 3; i++ {
    go func(n int) { fmt.Println(n) }(i)   // 正确
}

(Go 1.22 起循环变量每轮独立,但显式传参仍是好习惯。)

2. goroutine 泄漏——goroutine 阻塞在无人接收的 channel 上永不退出。确保每条路径都有退出机制(context 或 close)。

3. 数据竞争——多个 goroutine 无保护地读写同一变量。用 go run -race 检测:

$ go run -race main.go

竞态检测器会报告冲突位置,开发阶段务必跑。

七、小结

  • go 开 goroutine,main 退出会杀掉所有 goroutine
  • channel 通信,无缓冲同步、有缓冲解耦;发送方负责 close
  • select + time.After 做超时和非阻塞
  • sync.WaitGroup/Mutex/Once 处理同步
  • context 管生命周期,陷阱用 -race 兜底

练习:写一个函数并发请求 3 个模拟接口(time.Sleep 模拟耗时),用 select + context.WithTimeout 在 2 秒内收集结果,超时则返回错误。

下一篇讲工程实践:测试、泛型与项目组织——从"会写代码"走向"能维护项目"。

posted @ 2026-11-01 20:32  sunlon  阅读(2)  评论(0)    收藏  举报