🐸 青蛙编程网
首页 / Go / Go 基础 / 7. 并发

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 动手改

  1. 用 5 个 goroutine 各打 100 次随机数到同一个 channel,主循环收 500 次
  2. WaitGroup 版本改写上面的抓 URL(不用 results 收尾长循环)
  3. 故意去掉 time.Sleep 后 main 直接退出,观察「无输出」情况

7.6 小结

  • go 函数() = 并发;goroutine 大军压境也不怕
  • channel 是传话筒:发送 ch <-、接收 <-ch,双向阻塞同步节奏
  • 并行收尾三板斧:channel 收结果 / WaitGroup 等齐 / 循环传参避坑

下一章:综合练习:命令行小工具 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。