文章目录

Go 并发编程:goroutine 与 channel

发布于 2026-04-03 18:18:18 · 浏览 3 次 · 评论 0 条

Go 并发编程:goroutine 与 channel

Go 语言的并发模型基于两个核心概念:goroutinechannel。goroutine 是轻量级线程,由 Go 运行时自动管理;channel 是 goroutine 之间通信的管道,用于安全地传递数据。掌握这两者,就能高效编写并发程序。


启动一个 goroutine

  1. 定义一个普通函数,例如 func task() { ... }
  2. 在函数调用前加上 go 关键字,即可将其作为 goroutine 异步执行。
package main

import (
    "fmt"
    "time"
)

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

func main() {
    go say("world")  // 启动 goroutine
    say("hello")
}

上述代码中,go say("world") 会立即返回,主 goroutine 继续执行 say("hello")。两个任务并发运行,输出顺序不固定。

⚠️ 注意:如果主函数提前退出,所有 goroutine 会被强制终止。确保主 goroutine 等待其他 goroutine 完成。


使用 channel 通信

channel 是类型安全的队列,用于在 goroutine 之间发送和接收数据。

创建 channel

使用 make 函数创建 channel

ch := make(chan int)  // 创建一个传递 int 类型的 channel

发送与接收

  • 向 channel 发送数据ch <- value
  • 从 channel 接收数据value := <-ch
func main() {
    ch := make(chan string)

    go func() {
        ch <- "done"  // 发送字符串到 channel
    }()

    msg := <-ch  // 阻塞等待接收
    fmt.Println(msg)
}

此例中,主 goroutine 在 <-ch 处阻塞,直到匿名 goroutine 发送 "done"


控制并发执行流程

使用带缓冲的 channel

默认 channel 是无缓冲的(同步),发送和接收必须同时就绪。创建带缓冲的 channel 可以解耦发送与接收

ch := make(chan int, 2)  // 缓冲区大小为 2
ch <- 1
ch <- 2
// 此时再发送会阻塞,因为缓冲区已满
fmt.Println(<-ch)  // 接收一个,缓冲区空出一个位置

缓冲 channel 允许发送方在接收方未就绪时继续运行,最多可缓存指定数量的值。

使用 select 多路复用

当需要监听多个 channel 时,使用 select 语句

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() { ch1 <- "one" }()
    go func() { ch2 <- "two" }()

    select {
    case msg1 := <-ch1:
        fmt.Println("Received", msg1)
    case msg2 := <-ch2:
        fmt.Println("Received", msg2)
    }
}

select 会随机选择一个已就绪的 case 执行。若多个 channel 同时就绪,Go 运行时会公平调度。


安全关闭 channel

只有发送方应关闭 channel,表示“不再发送数据”。接收方可通过“逗号 ok”模式检测 channel 是否关闭。

ch := make(chan int)
go func() {
    for i := 0; i < 3; i++ {
        ch <- i
    }
    close(ch)  // 发送完毕后关闭
}()

for {
    if val, ok := <-ch; ok {
        fmt.Println(val)
    } else {
        break  // channel 已关闭,退出循环
    }
}

或者更简洁地使用 range 遍历 channel,自动在关闭后退出:

for val := range ch {
    fmt.Println(val)
}

❌ 错误做法:从多个 goroutine 同时关闭同一个 channel,会导致 panic。


常见模式:工作池(Worker Pool)

利用 goroutine 和 channel 实现并发任务处理。

  1. 创建任务 channel:用于分发任务。
  2. 启动多个 worker goroutine:每个 worker 从任务 channel 读取并处理。
  3. 主 goroutine 发送任务,完成后关闭 channel。
func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, job)
        results <- job * 2  // 模拟处理
    }
}

func main() {
    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    // 启动 3 个 worker
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // 发送任务
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)  // 所有任务已发送

    // 收集结果
    for a := 1; a <= numJobs; a++ {
        <-results
    }
}

该模式实现了任务的并发处理与结果收集,适用于批量作业场景。


避免常见陷阱

问题 表现 解决方法
主 goroutine 提前退出 子 goroutine 未执行完就被终止 使用 sync.WaitGroup 或 channel 同步
向已关闭 channel 发送 panic: send on closed channel 确保只有发送方关闭,且关闭前不再发送
从 nil channel 接收/发送 永久阻塞 初始化 channel 或检查是否为 nil
忘记关闭 channel range 循环无法退出 明确责任方,在数据发送完毕后关闭

例如,使用 sync.WaitGroup 等待所有 goroutine 完成:

var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    // 执行任务
}()
wg.Wait()  // 阻塞直到所有 Done 被调用

性能与调试建议

  • 不要盲目创建大量 goroutine:虽然 goroutine 开销小(初始栈约 2KB),但过多仍会消耗内存。

  • 使用 GOMAXPROCS 控制并行度:默认等于 CPU 核心数,可通过 runtime.GOMAXPROCS(n) 调整。

  • 启用竞态检测:编译时添加 -race 标志,可发现数据竞争问题:

    go run -race main.go
  • 避免共享内存:优先通过 channel 传递数据,而非共享变量加锁。

Go 的并发哲学是:“不要通过共享内存来通信,而应通过通信来共享内存”。坚持这一原则,能写出更清晰、更安全的并发代码。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文