← 返回首页

Go 并发编程:Goroutine 与 Channel

发布于 2025-01-05 | 作者:水码 | 阅读约 12 分钟

Go 语言的并发模型是其最大的卖点之一。不同于传统的线程模型,Go 通过 Goroutine(轻量级协程)和 Channel(通道)实现了优雅的 CSP(Communicating Sequential Processes)并发模式。本文将深入探讨 Go 并发编程的核心概念与最佳实践。

一、Goroutine:轻量级并发

Goroutine 是 Go 运行时管理的轻量级线程,创建成本极低(约 2KB 栈空间):

package main

import (
    "fmt"
    "time"
)

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

func main() {
    // 使用 go 关键字启动 Goroutine
    go sayHello("Alice")
    go sayHello("Bob")

    // 主 Goroutine 需要等待,否则程序直接退出
    time.Sleep(500 * time.Millisecond)
    fmt.Println("Done!")
}

⚠️ 注意:time.Sleep 不是正确的同步方式,仅用于演示。

二、Channel:Goroutine 间的通信

Go 的哲学是"不要通过共享内存来通信,而要通过通信来共享内存":

1. 基本用法

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

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

// 接收数据(阻塞,直到有数据)
value := <-ch
fmt.Println(value) // 42

2. 带缓冲的 Channel

// 创建缓冲大小为 3 的 Channel
ch := make(chan string, 3)

// 在缓冲未满时,发送不会阻塞
ch <- "A"
ch <- "B"
ch <- "C"
// ch <- "D"  // 这里会阻塞,因为缓冲已满

fmt.Println(<-ch) // "A"
fmt.Println(<-ch) // "B"

3. 关闭 Channel

ch := make(chan int, 5)

// 生产者
go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch) // 关闭 Channel,表示不再发送数据
}()

// 消费者:使用 range 遍历,Channel 关闭时自动退出
for value := range ch {
    fmt.Println(value)
}

// 检测 Channel 是否关闭
value, ok := <-ch
if !ok {
    fmt.Println("Channel 已关闭")
}

三、select:多路复用

select 语句用于同时监听多个 Channel:

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

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "来自 ch1"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "来自 ch2"
    }()

    // 等待多个 Channel,哪个先就绪就执行哪个
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }
}

超时处理

select {
case result := <-ch:
    fmt.Println("收到结果:", result)
case <-time.After(3 * time.Second):
    fmt.Println("操作超时")
}

非阻塞操作

select {
case msg := <-ch:
    fmt.Println("收到:", msg)
default:
    fmt.Println("没有数据,继续执行其他逻辑")
}

四、sync 包:同步原语

1. WaitGroup:等待一组 Goroutine 完成

import "sync"

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1) // 计数器 +1
        go func(id int) {
            defer wg.Done() // 计数器 -1
            fmt.Printf("Worker %d 完成\n", id)
        }(i)
    }

    wg.Wait() // 阻塞直到计数器归零
    fmt.Println("所有任务完成")
}

2. Mutex:互斥锁

import "sync"

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
}

3. Once:只执行一次

var once sync.Once
var instance *Database

func GetDatabase() *Database {
    once.Do(func() {
        instance = &Database{}
        instance.Connect()
    })
    return instance
}

五、并发模式实战

1. Worker Pool(工作池)

func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        fmt.Printf("Worker %d 处理任务 %d\n", id, job)
        time.Sleep(time.Second)
        results <- job * 2
    }
}

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

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

    // 发送 9 个任务
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)

    // 收集结果
    for r := 1; r <= 9; r++ {
        fmt.Println("结果:", <-results)
    }
}

2. Fan-out/Fan-in(扇出/扇入)

// 扇出:一个输入,多个处理者
func fanOut(input <-chan int, workers int) []<-chan int {
    channels := make([]<-chan int, workers)
    for i := 0; i < workers; i++ {
        channels[i] = process(input)
    }
    return channels
}

// 扇入:多个输入,合并为一个输出
func fanIn(channels ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int)

    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(ch)
    }

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

    return out
}

六、常见陷阱与最佳实践

  1. 避免 Goroutine 泄露:确保 Goroutine 有退出条件;
  2. 使用 context 控制生命周期:传递取消信号;
  3. Channel 方向限制:使用 chan<-<-chan 明确意图;
  4. 优先使用 Channel:而非共享内存 + 锁;
  5. 使用 race detectorgo run -race main.go

七、学习资源

Go 的并发模型优雅而强大,但也需要谨慎使用。理解 Goroutine 调度、Channel 阻塞特性、以及常见的并发模式,将帮助你写出高效、安全的并发代码。动手实践是最好的学习方式!