7. 并发:goroutine 与 channel
这是什么:并发(concurrency) = 让多个任务同时推进;Go 用 goroutine(轻量线程)和 channel(管道)两件套实现。
为什么用:上一章的程序一次只干一件事,再多请求也只能排队。真实服务端要同时接住成千上万个请求,而 Go 里起一个并发任务只多一个 go 关键字——这是它最省事的地方。
如何用:先给一个函数加上 go 让它并发跑,再用 ch <- 和 <-ch 把结果传回来,最后用 WaitGroup 等所有 goroutine 收工。
7.1 goroutine:轻量到起不完
func work(name string) {
fmt.Println(name, "完成")
}
func main() {
go work("任务A") // 加个 go = 并发执行
go work("任务B")
time.Sleep(time.Second) // 主任务结束会带走未完成的 goroutine
}
任务A 完成
任务B 完成
goroutine 极轻(几 KB 栈),一次起 10 万个都不夸张——传统线程这是不可想象。
7.2 channel:goroutine 之间的传话筒
「不要通过共享内存来通信,而要通过通信来共享内存」——Go 的并发箴言。
ch := make(chan string) // 传 string 的管道
go func() { ch <- "任务结果" }() // 发送
msg := <-ch // 接收(阻塞直到有值)
fmt.Println(msg)
任务结果
带缓冲:
ch := make(chan int, 3) // 缓冲 3 个,进不满先不阻塞
7.3 实战:并发抓取三个 URL
urls := []string{"a.com", "b.com", "c.com"}
results := make(chan string)
for _, u := range urls {
go func(url string) {
resp, err := http.Get("https://" + url)
if err != nil {
results <- url + " 失败"
return
}
resp.Body.Close()
results <- url + " OK"
}(u) // 注意:循环里的 go 要传参避免变量共享
}
for range urls {
fmt.Println(<-results) // 收三次结果
}
你会看到:三请求并发进行,总耗时接近最慢那个而非三者相加。
循环里起 goroutine 必传参(
go f(url)),否则闭包共享同一个变量——第一坑。
7.4 WaitGroup:等齐了再走
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done() // 完一个减一
fmt.Println("处理", url)
}(u)
}
wg.Wait() // 主任务在此等全部完成
7.5 动手改
- 用 5 个 goroutine 各打 100 次随机数到同一个 channel,主循环收 500 次
- WaitGroup 版本改写上面的抓 URL(不用 results 收尾长循环)
- 故意去掉
time.Sleep后 main 直接退出,观察「无输出」情况
7.6 小结
go 函数()= 并发;goroutine 大军压境也不怕- channel 是传话筒:发送
ch <-、接收<-ch,双向阻塞同步节奏 - 并行收尾三板斧:channel 收结果 / WaitGroup 等齐 / 循环传参避坑
下一章:综合练习:命令行小工具 🐸