GO语言基础教程(142)Go并发编程概述:Go并发魔法:让你的代码坐上火箭飙升!
来源:     阅读:4
易浩激活码
发布于 2025-11-03 18:18
查看主页

兄弟们,姐妹们,如果你还在为程序运行太慢而抓耳挠腮,如果你看着电脑的多核CPU却不知道怎么物尽其用,那么恭喜你,发现宝藏了!今天我要带你领略Go语言中最酷炫的特性——并发编程。这可不是那种让人头秃的复杂技术,Go的并发简直像魔法一样简单有趣!

为什么需要并发?先来个灵魂拷问

想象一下这个场景:你要准备一顿丰盛晚餐,如果按照传统方式——先洗菜10分钟,然后切菜10分钟,接着炒菜20分钟,最后煮饭15分钟——总共需要55分钟。饿都得饿趴下了!

但现实中你怎么做?肯定是米饭先煮上,然后同时洗菜、切菜,最后炒菜。这样可能30分钟就搞定了!这就是并发的好处——在同一时间段内做多件事,效率直接翻倍。

在编程世界里也是同样的道理。传统的顺序执行就像那个笨拙的厨师,而并发编程让你化身超级大厨,同时处理多个任务,让你的程序性能蹭蹭往上涨!

Goroutine:Go并发的灵魂所在

在别的语言里,搞并发可能要创建线程,配置线程池,处理各种锁... 光想想就头大。但在Go里?简单到让你怀疑人生!

Goroutine是什么? 你可以把它理解成“超级轻量级的线程”。创建一个goroutine的成本极低,只需要几KB内存,而且创建和销毁都由Go运行时管理,完全不用你操心。

看看这迷人的语法:



func main() {
    // 普通函数调用:吃完一份再吃下一份
    eatBreakfast()
    eatLunch()
    
    // 使用goroutine并发执行:早餐午餐一起吃!
    go eatBreakfast()
    go eatLunch()
    
    // 给点时间让goroutine执行完
    time.Sleep(time.Second)
}
 
func eatBreakfast() {
    fmt.Println("开始享用早餐...")
    time.Sleep(500 * time.Millisecond)
    fmt.Println("早餐吃完了!")
}
 
func eatLunch() {
    fmt.Println("开始享用午餐...")
    time.Sleep(300 * time.Millisecond)
    fmt.Println("午餐吃完了!")
}

看到那个神奇的 go关键字了吗?就这么简单的一个前缀,普通函数调用就变成了并发执行!

但等等,这里有个问题——我们用了 time.Sleep来等待goroutine完成。这就像你知道朋友吃饭大概需要1小时,所以定个1小时后的闹钟再回来找他。这种方式太low了,而且不准确!

Channel:goroutine之间的通信桥梁

Go语言有一句名言:“不要通过共享内存来通信,而应该通过通信来共享内存。” channel就是实现这一理念的神器。

Channel是什么? 想象一下传送带:一个goroutine把东西放上去,另一个goroutine从上面取走。安全、高效、不会乱!

来,看看channel的正确打开方式:



func main() {
    // 创建一个传送带(channel)
    done := make(chan bool)
    
    go func() {
        fmt.Println("我在后台默默干活...")
        time.Sleep(time.Second)
        fmt.Println("活干完了!")
        done <- true // 往传送带上放个信号
    }()
    
    <-done // 从传送带上取信号,取不到就等着
    fmt.Println("收到完成信号,主程序结束")
}

这个例子中,主程序会在 <-done这里耐心等待,直到goroutine完成任务发送信号。优雅!

实战:用并发爬虫感受速度与激情

光说不练假把式,来点真家伙!假设你要抓取多个网站的信息,顺序执行得等半天,但用并发... 感受下什么叫速度!



package main
 
import (
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)
 
// 网站结构体
type Website struct {
    URL     string
    Content string
}
 
func main() {
    // 要抓取的网站列表
    websites := []string{
        "https://www.baidu.com",
        "https://www.zhihu.com", 
        "https://github.com",
        "https://stackoverflow.com",
    }
    
    start := time.Now()
    results := sequentialCrawl(websites) // 顺序抓取
    elapsed := time.Since(start)
    fmt.Printf("顺序抓取完成,耗时: %v
", elapsed)
    
    start = time.Now()
    results = concurrentCrawl(websites) // 并发抓取
    elapsed = time.Since(start)
    fmt.Printf("并发抓取完成,耗时: %v
", elapsed)
    
    // 显示结果摘要
    for _, site := range results {
        fmt.Printf("网站: %s, 内容长度: %d字节
", site.URL, len(site.Content))
    }
}
 
// 顺序抓取 - 像老牛拉车
func sequentialCrawl(urls []string) []Website {
    var results []Website
    for _, url := range urls {
        resp, err := http.Get(url)
        if err != nil {
            fmt.Printf("抓取 %s 失败: %v
", url, err)
            continue
        }
        
        content, _ := ioutil.ReadAll(resp.Body)
        results = append(results, Website{URL: url, Content: string(content)})
        resp.Body.Close()
    }
    return results
}
 
// 并发抓取 - 像坐火箭
func concurrentCrawl(urls []string) []Website {
    // 创建传送带(buffered channel)
    results := make(chan Website, len(urls))
    
    for _, url := range urls {
        go func(u string) {
            resp, err := http.Get(u)
            if err != nil {
                fmt.Printf("抓取 %s 失败: %v
", u, err)
                results <- Website{URL: u, Content: ""}
                return
            }
            
            content, _ := ioutil.ReadAll(resp.Body)
            results <- Website{URL: u, Content: string(content)}
            resp.Body.Close()
        }(url) // 注意这里要传参,避免循环变量捕获问题
    }
    
    // 收集所有结果
    var websites []Website
    for i := 0; i < len(urls); i++ {
        websites = append(websites, <-results)
    }
    
    return websites
}

运行这个程序,你会看到神奇的结果:并发版本的速度几乎是顺序版本的4倍!这就是并发的魔力——多个网站同时抓取,而不是一个一个等。

进阶技巧:WaitGroup让代码更优雅

上面的例子中,我们通过channel来收集结果,但有时候我们只需要知道所有goroutine都完成了,不关心具体结果。这时候 sync.WaitGroup就派上用场了。



package main
 
import (
    "fmt"
    "sync"
    "time"
)
 
func main() {
    var wg sync.WaitGroup
    workerCount := 5
    
    fmt.Println("老板:开始今天的工作!")
    
    for i := 1; i <= workerCount; i++ {
        wg.Add(1) // 登记一个工人
        go worker(i, &wg)
    }
    
    fmt.Println("老板:我在办公室喝茶等着...")
    wg.Wait() // 等所有工人完工
    fmt.Println("老板:活都干完了,下班!")
}
 
func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 干完活记得销假
    
    fmt.Printf("工人%d: 开始干活了!
", id)
    time.Sleep(time.Duration(id) * time.Second) // 模拟工作时间
    fmt.Printf("工人%d: 活干完了!
", id)
}

WaitGroup就像公司的考勤系统: Add()是登记上班, Done()是登记下班, Wait()是老板等着所有人都下班。

并发安全:别让goroutine打架

多个goroutine同时操作共享数据时,可能会出乱子。比如两个人都想同时更新同一个银行账户余额...



package main
 
import (
    "fmt"
    "sync"
)
 
func main() {
    // 不安全的并发访问
    unsafeCounter()
    
    // 安全的并发访问  
    safeCounter()
}
 
func unsafeCounter() {
    counter := 0
    var wg sync.WaitGroup
    
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++ // 多个goroutine同时修改,会出问题!
        }()
    }
    
    wg.Wait()
    fmt.Printf("不安全计数结果: %d (期望: 1000)
", counter)
}
 
func safeCounter() {
    counter := 0
    var wg sync.WaitGroup
    var mutex sync.Mutex // 互斥锁 - 像卫生间门锁
    
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            
            mutex.Lock()   // 拿到锁才能进去
            counter++     // 安全地修改
            mutex.Unlock() // 出来释放锁
        }()
    }
    
    wg.Wait()
    fmt.Printf("安全计数结果: %d (期望: 1000)
", counter)
}

运行这个程序,你会看到不安全版本的结果可能不是1000,而安全版本始终是1000。互斥锁( sync.Mutex)确保同一时间只有一个goroutine能访问临界区。

生产者和消费者模式:经典永流传

这是并发编程中最常用的模式之一,特别适合处理数据流水线。



package main
 
import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)
 
func main() {
    // 创建数据通道
    jobs := make(chan int, 10)
    results := make(chan int, 10)
    
    // 启动消费者(工人)
    var wg sync.WaitGroup
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, jobs, results, &wg)
    }
    
    // 生产者(老板)发布任务
    go func() {
        for i := 1; i <= 10; i++ {
            jobs <- i // 发送任务
            fmt.Printf("老板: 发布了任务%d
", i)
        }
        close(jobs) // 任务发完了,关闭通道
    }()
    
    // 等待所有工人完成
    go func() {
        wg.Wait()
        close(results) // 所有结果都产生了
    }()
    
    // 收集结果
    for result := range results {
        fmt.Printf最终结果: 任务处理完成,结果=%d
", result)
    }
    
    fmt.Println("所有工作完成!")
}

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    
    for job := range jobs {
        fmt.Printf("工人%d: 正在处理任务%d
", id, job)
        time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) // 模拟处理时间
        results <- job * 2 // 处理结果是输入的两倍
        fmt.Printf("工人%d: 完成任务%d
", id, job)
    }
}

这个模式的美妙之处在于:生产者和消费者完全解耦,可以独立调整各自的数量,系统很容易扩展。

并发编程避坑指南

新手玩并发,容易踩这些坑:

循环变量捕获:在goroutine中使用循环变量时,要记得传参!


// 错误写法
for i := 0; i < 5; i++ {
    go func() {
        fmt.Println(i) // 可能全部输出5!
    }()
}
 
// 正确写法  
for i := 0; i < 5; i++ {
    go func(num int) {
        fmt.Println(num) // 正确输出0-4
    }(i)
}
Channel死锁:没有正确的发送接收配对会导致程序卡死


ch := make(chan int)
ch <- 42 // 发送了数据,但没有接收者,卡在这里!
fmt.Println(<-ch)
资源泄露:记得关闭不再使用的channel,避免goroutine泄露

结语:拥抱并发新时代

Go语言的并发模型之所以这么出色,是因为它从语言层面就提供了支持,而不是像其他语言那样通过库的方式后补。goroutine的轻量级、channel的优雅、select的多路复用... 这些特性让并发编程从痛苦的折磨变成了愉快的享受。

记住,并发的核心思想是:把复杂的大任务拆分成可以并行执行的小任务。就像准备晚餐一样,合理安排,同时进行,效率自然提升。

现在你已经掌握了Go并发编程的基础武器,是时候在你的项目中大展身手了!让你的代码坐上火箭,体验极速飞驰的感觉吧!


动手练习:尝试修改上面的爬虫示例,让它能够同时抓取更多网站,并比较顺序执行和并发执行的性能差异。你会发现,并发越多,速度优势越明显!(当然也要注意别把人家网站爬挂了...)

Happy coding!愿你的程序永远并发,永不阻塞!

免责声明:本文为用户发表,不代表网站立场,仅供参考,不构成引导等用途。 系统环境
相关推荐
Scala 入门
前台初级新人,如何撕去菜鸟标签
input/textarea 监测value改变究竟该用什么事件
华为、思科、华三 路由器ip地址统计小工具编码
绝了!这款工具让SpringBoot不再需要Controller、Service、DAO、Mapper!
首页
搜索
订单
购物车
我的