在这里插入代码片# Go语言并发编程详解:goroutine和channel实战

Go 语言天生支持并发,这得益于其轻量级的 goroutine 和强大的 channel 机制。本文将深入讲解 goroutine 和 channel 的使用方法,通过多个实战代码示例帮助你掌握 Go 并发编程的精髓。

一、为什么 Go 适合并发编程

Go 语言在设计之初就考虑了并发。它的并发模型基于 CSP(Communicating Sequential Processes),核心思想是:不要通过共享内存来通信,而要通过通信来共享内存

Go 并发的两大基石:

  • goroutine:轻量级线程,创建成本极低
  • channel:goroutine 之间安全通信的管道

二、goroutine 详解

2.1 什么是 goroutine

goroutine 是 Go 中的轻量级执行单元。与操作系统线程相比:

  • 创建一个 goroutine 只需约 2KB 栈空间
  • 线程通常需要 1-8MB 栈空间
  • goroutine 由 Go 运行时调度,而非操作系统

2.2 创建 goroutine

package main

import (
    "fmt"
    "time"
)

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

func main() {
    // 使用 go 关键字启动 goroutine
    go sayHello("Goroutine-1")
    go sayHello("Goroutine-2")

    // 主 goroutine 也执行
    sayHello("Main")

    // 等待其他 goroutine 完成
    time.Sleep(time.Second)
    fmt.Println("All done!")
}

2.3 goroutine 的调度

Go 使用 GMP 调度模型:

  • G:goroutine
  • M:操作系统线程
  • P:处理器(调度上下文)
package main

import (
    "fmt"
    "runtime"
    "sync"
)

func main() {
    // 设置使用多个 CPU 核心
    runtime.GOMAXPROCS(runtime.NumCPU())

    var wg sync.WaitGroup
    wg.Add(3)

    for i := 1; i <= 3; i++ {
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Worker %d running on CPU\n", id)
        }(i)
    }

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

三、channel 详解

3.1 什么是 channel

channel 是 goroutine 之间通信的管道。它是类型安全的,发送和接收操作都会阻塞,直到另一端准备好。

3.2 基本使用

package main

import "fmt"

func main() {
    // 创建一个无缓冲 channel
    ch := make(chan string)

    // 启动 goroutine 发送数据
    go func() {
        ch <- "Hello from goroutine!"
    }()

    // 主 goroutine 接收数据
    msg := <-ch
    fmt.Println(msg)
}

3.3 缓冲 channel

package main

import "fmt"

func main() {
    // 创建一个缓冲大小为 3 的 channel
    ch := make(chan int, 3)

    // 可以连续发送 3 个数据而不会阻塞
    ch <- 1
    ch <- 2
    ch <- 3

    // 接收数据
    fmt.Println(<-ch) // 输出: 1
    fmt.Println(<-ch) // 输出: 2
    fmt.Println(<-ch) // 输出: 3

    // 缓冲区已空,再接收会阻塞
    // fmt.Println(<-ch) // deadlock!
}

3.4 channel 的方向

package main

import "fmt"

// 只能发送的 channel
func producer(out chan<- int) {
    for i := 0; i < 5; i++ {
        out <- i * 10
    }
    close(out)
}

// 只能接收的 channel
func consumer(in <-chan int) {
    for num := range in {
        fmt.Printf("Received: %d\n", num)
    }
}

func main() {
    ch := make(chan int, 5)
    go producer(ch)
    consumer(ch)
}

四、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 <- "Message from ch1"
    }()

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

    // 使用 select 等待多个 channel
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }
}

4.1 超时处理

func waitForData(ch chan string) {
    select {
    case data := <-ch:
        fmt.Printf("Received: %s\n", data)
    case <-time.After(3 * time.Second):
        fmt.Println("Timeout! No data received.")
    }
}

五、实战:工作池(Worker Pool)

工作池是 Go 并发编程中最经典的模式之一:

package main

import (
    "fmt"
    "time"
)

// Worker 处理任务
func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        fmt.Printf("Worker %d started job %d\n", id, job)
        time.Sleep(time.Second) // 模拟耗时操作
        results <- job * job    // 返回平方值
    }
}

func main() {
    const numJobs = 10
    const numWorkers = 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

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

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

    // 收集结果
    for r := 1; r <= numJobs; r++ {
        fmt.Printf("Result: %d\n", <-results)
    }
}

六、实战:扇出扇入(Fan-out Fan-in)

package main

import (
    "fmt"
    "sync"
)

// 生成数据
func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            out <- n
        }
    }()
    return out
}

// 处理数据(平方)
func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            out <- n * n
        }
    }()
    return out
}

// 合并多个 channel
func merge(cs ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int)

    output := func(c <-chan int) {
        defer wg.Done()
        for n := range c {
            out <- n
        }
    }

    wg.Add(len(cs))
    for _, c := range cs {
        go output(c)
    }

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

func main() {
    in := generate(1, 2, 3, 4, 5)

    // Fan-out: 启动多个 square 处理器
    c1 := square(in)
    c2 := square(in)

    // Fan-in: 合并结果
    for result := range merge(c1, c2) {
        fmt.Printf("Result: %d\n", result)
    }
}

七、性能对比

我们对比串行和并发处理同样任务的性能:

package main

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

func slowTask(id int) int {
    time.Sleep(100 * time.Millisecond)
    return id * id
}

// 串行处理
func serialProcess(count int) int {
    total := 0
    for i := 0; i < count; i++ {
        total += slowTask(i)
    }
    return total
}

// 并发处理
func concurrentProcess(count int) int {
    var wg sync.WaitGroup
    results := make(chan int, count)

    for i := 0; i < count; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            results <- slowTask(id)
        }(i)
    }

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

    total := 0
    for r := range results {
        total += r
    }
    return total
}

func main() {
    count := 20

    start := time.Now()
    serialResult := serialProcess(count)
    serialTime := time.Since(start)
    fmt.Printf("串行: 结果=%d, 耗时=%v\n", serialResult, serialTime)

    start = time.Now()
    concurrentResult := concurrentProcess(count)
    concurrentTime := time.Since(start)
    fmt.Printf("并发: 结果=%d, 耗时=%v\n", concurrentResult, concurrentTime)

    fmt.Printf("加速比: %.2fx\n", float64(serialTime)/float64(concurrentTime))
}

运行结果示例:

串行: 结果=2470, 耗时=2.0031245s
并发: 结果=2470, 耗时=100.5234ms
加速比: 19.92x

可以看到,并发处理比串行处理快了约 20 倍

八、常见陷阱与注意事项

8.1 goroutine 泄漏

// 错误示例:goroutine 泄漏
func leakExample() {
    ch := make(chan int)
    go func() {
        val := <-ch // 如果没有发送者,这个 goroutine 会永远阻塞
        fmt.Println(val)
    }()
    // 函数返回后,goroutine 仍在等待,造成泄漏
}

8.2 正确关闭 channel

// 只有发送者应该关闭 channel
func producer(ch chan<- int) {
    defer close(ch) // 正确:发送者关闭
    for i := 0; i < 10; i++ {
        ch <- i
    }
}

8.3 使用 sync.WaitGroup 等待

var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        // do work
    }(i)
}
wg.Wait() // 等待所有 goroutine 完成

九、总结

特性 goroutine 操作系统线程
创建成本 ~2KB ~1-8MB
调度方式 Go运行时 操作系统
切换成本 极低 较高
数量上限 十万级 千级

Go 并发编程的核心要点:

  1. 用 go 关键字启动 goroutine
  2. 用 channel 在 goroutine 间通信
  3. 用 select 处理多路 channel
  4. 用 sync.WaitGroup 等待 goroutine 完成
  5. 避免 goroutine 泄漏和死锁

掌握 goroutine 和 channel,你就掌握了 Go 语言最强大的特性。开始写你的并发程序吧!


如果这篇文章对你有帮助,欢迎点赞、收藏、关注! 后续会分享更多 Go 语言进阶教程,包括 context 包的使用、并发模式深入等内容。有问题欢迎评论区交流!

作者:tlyyxjz | 转载请注明出处

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐