返回文章列表
技术2026-03-10

Go 语言并发编程精讲

Goroutine、Channel、Mutex 与 Context——Go 并发原语完全指南。

引言

并发是 Go 语言最强大的特性之一。Go 的并发模型基于 CSP(Communicating Sequential Processes),核心理念是"不要通过共享内存来通信,而要通过通信来共享内存"。本文从 Goroutine 和 Channel 的基础出发,逐步深入到锁、Context 和 Worker Pool 等高级模式。

一、Goroutine:轻量级线程

Goroutine 是 Go 运行时管理的轻量级线程,初始栈大小只有 2KB(相比之下,操作系统线程通常为 1MB),这使得你可以在单机上轻松运行数十万个 Goroutine。

package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    for i := 0; i < 3; i++ {
        fmt.Printf("Hello, %s!\n", name)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go sayHello("Alice")  // 启动一个 Goroutine
    go sayHello("Bob")    // 启动另一个 Goroutine

    time.Sleep(1 * time.Second)  // 等待 Goroutine 执行完毕
    fmt.Println("Done")
}

二、Channel:Goroutine 间的通信

Channel 是 Go 并发模型的核心,用于在 Goroutine 之间传递数据。

2.1 无缓冲 Channel

ch := make(chan int)  // 无缓冲 Channel

go func() {
    ch <- 42  // 发送:阻塞直到有接收者
}()

value := <-ch  // 接收:阻塞直到有发送者
fmt.Println(value)  // 42

2.2 有缓冲 Channel

ch := make(chan int, 3)  // 缓冲大小为 3

ch <- 1  // 不阻塞
ch <- 2  // 不阻塞
ch <- 3  // 不阻塞,缓冲区已满
// ch <- 4  // 阻塞!缓冲区已满,等待接收者

fmt.Println(<-ch)  // 1
fmt.Println(<-ch)  // 2
fmt.Println(<-ch)  // 3

2.3 Select 多路复用

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

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

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

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("Received:", msg1)
        case msg2 := <-ch2:
            fmt.Println("Received:", msg2)
        case <-time.After(3 * time.Second):
            fmt.Println("Timeout!")
            return
        }
    }
}

三、同步原语

3.1 sync.Mutex 和 sync.RWMutex

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

// RWMutex:读多写少场景
type SafeMap struct {
    mu   sync.RWMutex
    data map[string]string
}

func (m *SafeMap) Get(key string) string {
    m.mu.RLock()         // 读锁,允许多个 Goroutine 同时持有
    defer m.mu.RUnlock()
    return m.data[key]
}

func (m *SafeMap) Set(key, value string) {
    m.mu.Lock()          // 写锁,独占
    defer m.mu.Unlock()
    m.data[key] = value
}

3.2 sync.WaitGroup

func main() {
    var wg sync.WaitGroup
    urls := []string{"https://golang.org", "https://google.com"}

    for _, url := range urls {
        wg.Add(1)  // 增加计数
        go func(u string) {
            defer wg.Done()  // 完成时减 1
            resp, err := http.Get(u)
            if err != nil {
                fmt.Printf("Error fetching %s: %v\n", u, err)
                return
            }
            fmt.Printf("%s: %d\n", u, resp.StatusCode)
            resp.Body.Close()
        }(url)
    }

    wg.Wait()  // 等待所有 Goroutine 完成
    fmt.Println("All done")
}

四、context.Context:取消与超时

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d: cancelled\n", id)
            return
        default:
            fmt.Printf("Worker %d: working...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    // 带超时的 Context
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    for i := 1; i <= 3; i++ {
        go worker(ctx, i)
    }

    <-ctx.Done()  // 等待 2 秒超时
    fmt.Println("Main: timeout reached")
}

五、Worker Pool 模式

func workerPool(numWorkers int, jobs <-chan int, results chan<- int) {
    var wg sync.WaitGroup

    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for job := range jobs {
                fmt.Printf("Worker %d processing job %d\n", id, job)
                results <- job * 2  // 模拟处理
            }
        }(w)
    }

    go func() {
        wg.Wait()
        close(results)
    }()
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    workerPool(3, jobs, results)

    for j := 1; j <= 10; j++ {
        jobs <- j
    }
    close(jobs)

    for r := range results {
        fmt.Println("Result:", r)
    }
}

六、Race Detector

Go 内置了数据竞争检测器,在测试或开发环境中使用 -race 标志即可:

# 运行测试并检测数据竞争
go test -race ./...

# 运行程序并检测数据竞争
go run -race main.go

# 构建时启用竞争检测
go build -race -o myapp

总结

Go 的并发编程有两条路径:当需要共享状态时,使用 sync.Mutex 保护;当需要传递数据时,使用 Channel 通信。记住 Go 的并发格言:"Don't communicate by sharing memory; share memory by communicating." 在实际开发中,优先使用 Channel 和 Context 构建清晰的并发模式,只在必要时才使用 Mutex。最后,始终在测试中启用 -race 标志,数据竞争是并发编程中最隐蔽的 bug。


返回文章列表
标签:Go并发编程Goroutine