第二十五回:Go 并发

影分身之术(Goroutine & Channel)
唐伯虎
唐伯虎: 华府的工作太多了!又要扫地,又要画画,还要给秋香姐写诗。一个人根本干不过来!如果我会“影分身之术”,变出好几个我,同时干活,那该多好!

💡 核心概念: Go 语言的并发(Concurrency)是其最大的杀手锏。
1. Goroutine (协程):轻量级的线程,也就是“影分身”。
2. Channel (通道):协程之间通信的管道,也就是“传音入密”。
3. Sync (同步):确保分身们不会打架(数据竞争)。

🌪️ 第一式:影分身 (Goroutine)

在调用函数前加一个 go 关键字,就能瞬间启动一个分身去执行任务,而你自己(主线程)可以继续往下走,不用傻等。

package main
import (
    "fmt"
    "time"
)

func SweepFloor() {
    for i := 0; i < 5; i++ {
        fmt.Println("🧹 分身正在扫地...", i)
        time.Sleep(100 * time.Millisecond) // 模拟干活耗时
    }
}

func main() {
    // 启动一个分身去扫地
    go SweepFloor() 

    // 主身继续干别的
    for i := 0; i < 5; i++ {
        fmt.Println("🖌️ 主身正在画画...", i)
        time.Sleep(100 * time.Millisecond)
    }
}
秋香
秋香: 公子要注意哦!如果主身(main 函数)挂了(退出了),所有的影分身也会瞬间消失,不管它们活干完没干完。所以有时候要等等它们。

⏳ 第二式:原地待命 (WaitGroup)

time.Sleep 等待是不靠谱的。正规的做法是用 sync.WaitGroup 来点名。

import "sync"

var wg sync.WaitGroup // 定义一个计数器

func WashDishes() {
    defer wg.Done() // 干完活了,计数器减 1
    fmt.Println("🍽️ 洗碗完成!")
}

func main() {
    wg.Add(1) // 计数器加 1,表示有一个活要干
    go WashDishes()
    
    fmt.Println("主身在喝茶等待...")
    wg.Wait() // 阻塞在这里,直到计数器归零
    fmt.Println("所有活都干完了,收工!")
}

📡 第三式:传音入密 (Channel)

影分身之间怎么交换情报?不要通过共享内存(比如全局变量)来通信,而要通过通信来共享内存。这就是 Channel

func main() {
    // 创建一个传送字符串的通道
    // make(chan 数据类型)
    ch := make(chan string) 
    
    // 启动分身发送消息
    go func() {
        fmt.Println("分身:准备发送情书...")
        ch <- "秋香姐,我爱你!" // 把消息塞进管道
        fmt.Println("分身:发送完毕")
    }()
    
    // 主身接收消息
    msg := <-ch // 阻塞等待,直到管道里有东西出来
    fmt.Println("主身收到消息:", msg)
}
华夫人
华夫人: 哼!通道如果不带缓冲区(Unbuffered),发送方必须等到接收方准备好才能发出去,就像打电话,对方不接你就得一直等着!如果带缓冲区(Buffered),就像发短信,发过去就行了。

👂 第四式:听声辨位 (Select)

如果同时有好几个消息来源,怎么处理?Go 提供了 select 关键字,它可以同时监听多个通道。

func main() {
    qiuxiang := make(chan string)
    shiliu := make(chan string)

    go func() { time.Sleep(1 * time.Second); qiuxiang <- "公子,我是秋香" }()
    go func() { time.Sleep(2 * time.Second); shiliu <- "9527,我是石榴姐" }()

    // 谁的消息先到,就处理谁
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-qiuxiang:
            fmt.Println("😍 收到秋香的消息:", msg1)
        case msg2 := <-shiliu:
            fmt.Println("😱 收到石榴的消息:", msg2)
        }
    }
}

🔒 第五式:厨房争夺战 (Mutex 互斥锁)

如果多个分身同时抢着用厨房(修改同一个变量),就会乱套(数据竞争)。这时需要加锁。

import "sync"

var (
    rice = 0
    lock sync.Mutex // 厨房门锁
)

func AddRice() {
    lock.Lock()   // 进门先上锁
    defer lock.Unlock() // 出门解锁
    
    rice++
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            AddRice()
        }()
    }
    wg.Wait()
    fmt.Println("米缸里的米粒数:", rice) // 如果不加锁,这里可能不到 1000
}

📜 第六式:圣旨到 (Context)

如果皇上(主程序)下旨说“不用干了,退下”,所有分身必须立刻停止。Go 提供了 context 来实现这种超时控制和取消机制。

import (
    "context"
    "time"
)

func main() {
    // 创建一个带超时的上下文,3秒后自动取消
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go func(ctx context.Context) {
        select {
        case <-time.After(5 * time.Second):
            fmt.Println("任务完成!")
        case <-ctx.Done(): // 监听取消信号
            fmt.Println("🛑 收到圣旨:时间到,任务取消!")
        }
    }(ctx)

    time.Sleep(4 * time.Second)
}

🎯 练功房(左右互搏)

启动一个 goroutine 向通道发送数字 9527,主函数接收并打印。

package main
import "fmt"

func main() {
    c := make(chan int)
    
    // 填空:启动一个 goroutine
    ______ func() {
        c <- 9527
    }()
    
    // 接收数据
    fmt.Println(<-c)
}

任务: 启动协程的关键字是什么?

答案: go

解析: go 关键字用于启动一个新的 Goroutine。这是 Go 语言并发编程的基石。