Skip to content
奥运的 Blog
Go back

理解 Goroutine 与 Channel 的并发模型

Go 的并发模型基于 CSP(Communicating Sequential Processes)理论,核心思想是:不要通过共享内存来通信,而要通过通信来共享内存。这使得 Go 的并发代码既简洁又安全。

Goroutine 基础

Goroutine 是 Go 运行时管理的轻量级线程,创建成本极低(初始栈约 2-8KB),可以轻松启动数十万个:

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second) // 模拟工作
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers done")
}

Channel 通信

Channel 是 goroutine 之间通信的管道:

package main

import "fmt"

func sum(s []int, c chan int) {
    total := 0
    for _, v := range s {
        total += v
    }
    c <- total // 发送结果到 channel
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}
    c := make(chan int)

    go sum(s[:len(s)/2], c) // 前半部分
    go sum(s[len(s)/2:], c) // 后半部分

    x, y := <-c, <-c // 接收两个结果

    fmt.Println(x, y, x+y) // 17 -5 12(顺序可能不同)
}

带缓冲的 Channel

package main

import "fmt"

func main() {
    // 缓冲大小为 3,发送方在缓冲满之前不会阻塞
    ch := make(chan string, 3)

    ch <- "first"
    ch <- "second"
    ch <- "third"

    fmt.Println(<-ch) // first
    fmt.Println(<-ch) // second
    fmt.Println(<-ch) // third
}

生产者-消费者模式

这是 Channel 最经典的用法:

package main

import (
    "fmt"
    "sync"
)

func producer(jobs chan<- int, n int) {
    for i := 0; i < n; i++ {
        jobs <- i
        fmt.Printf("Produced: %d\n", i)
    }
    close(jobs) // 生产完毕,关闭 channel
}

func consumer(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs { // range 会在 channel 关闭后自动退出
        result := job * job // 模拟处理:计算平方
        results <- result
        fmt.Printf("Consumer %d processed job %d -> %d\n", id, job, result)
    }
}

func main() {
    jobs := make(chan int, 10)
    results := make(chan int, 10)
    var wg sync.WaitGroup

    // 启动 3 个消费者
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go consumer(i, jobs, results, &wg)
    }

    // 启动生产者
    go producer(jobs, 9)

    // 等待所有消费者完成后关闭 results
    go func() {
        wg.Wait()
        close(results)
    }()

    // 收集所有结果
    for result := range results {
        fmt.Println("Result:", result)
    }
}

select 语句

select 让 goroutine 可以同时等待多个 channel 操作:

package main

import (
    "fmt"
    "time"
)

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

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "from channel 1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "from channel 2"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("Received:", msg1)
        case msg2 := <-ch2:
            fmt.Println("Received:", msg2)
        }
    }
}

超时控制

使用 select + time.After 实现超时:

func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
    resultCh := make(chan string, 1)
    errCh := make(chan error, 1)

    go func() {
        // 模拟 HTTP 请求
        time.Sleep(2 * time.Second)
        resultCh <- "response data"
    }()

    select {
    case result := <-resultCh:
        return result, nil
    case err := <-errCh:
        return "", err
    case <-time.After(timeout):
        return "", fmt.Errorf("request timed out after %v", timeout)
    }
}

常见陷阱

1. 忘记关闭 Channel 导致泄漏

// 错误:如果没有人关闭 jobs,consumer 会永远阻塞
for job := range jobs { // range 需要 channel 被关闭才能退出
    process(job)
}

2. 向已关闭的 Channel 发送数据会 panic

ch := make(chan int)
close(ch)
ch <- 1 // panic: send on closed channel

3. goroutine 泄漏

// 如果 ch 没人接收,这个 goroutine 会永远阻塞
go func() {
    ch <- heavyComputation() // 如果主函数已退出或 ch 满了,这里永远阻塞
}()

总结

Go 的并发模型优雅且强大:

在实际项目中,要根据场景选择:简单的数据传递用 channel,需要多 goroutine 共享状态时用 sync.Mutex,需要传递请求上下文时用 context 包。


Share this post on:

Previous Post
使用 Gin 构建 RESTful API
Next Post
Go 语言入门:从 Hello World 到 HTTP 服务器