并发是 Go 语言最大的卖点之一:不用显式开线程、不用记复杂的线程 API,在函数调用前加一个 go 关键字,代码就并发跑起来了。这个轻量的执行单元就叫 goroutine。本篇带你从零学会启动、等待和协调 goroutine。

什么是 goroutine

goroutine 是由 Go 运行时调度的轻量级"用户态线程"。它和操作系统线程的区别在于:启动成本极低(初始栈只有几 KB),一个程序里同时跑成千上万个 goroutine 是家常便饭。你可以把它理解成"超轻量的并发任务"。

用 go 关键字启动并发

在普通函数调用前加上 go,这个函数就会在一个新的 goroutine 里执行,主流程不必等它结束:

package main

import (
    "fmt"
    "time"
)

func say(name string) {
    for i := 1; i <= 3; i++ {
        fmt.Println(name, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go say("goroutine") // 新 goroutine 里并发执行
    say("main")         // 主 goroutine 自己执行
}

输出会交错出现,类似:

main 1
goroutine 1
main 2
goroutine 2
main 3
goroutine 3

注意一个经典陷阱:把 go say(...) 的输出删掉后,程序可能什么都打印不出来就结束了——因为 main 一返回,所有还没跑完的 goroutine 会被直接终止,没人会等它们。这就引出下一个话题:如何等待。

用 sync.WaitGroup 等待所有任务完成

最常用的等待工具是 sync.WaitGroup:启动前 Add(1) 计数加一,任务结束时 Done() 计数减一,主流程 Wait() 阻塞到计数归零。

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 函数返回时计数减一
    fmt.Println("工人", id, "完成工作")
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, &wg) // 注意传指针,保证大家共用同一个 wg
    }
    wg.Wait()
    fmt.Println("全部完工")
}

输出顺序可能不同,但"全部完工"一定最后打印。两个细节:wg.Add(1) 必须在 go 语句之前调用;传给 goroutine 的必须是 &wg(指针),否则每个 goroutine 会拿到一份拷贝,Wait 永远等不到归零。

用 channel 在 goroutine 之间传数据

goroutine 之间不靠共享变量通信(容易出数据竞争),Go 推荐用 channel(通道)传递数据。无缓冲 channel 的收发会互相等待,天然形成同步:

package main

import "fmt"

func producer(ch chan int) {
    for i := 1; i <= 3; i++ {
        ch <- i // 发送数据进通道
    }
    close(ch) // 发送完毕,关闭通道
}

func main() {
    ch := make(chan int)
    go producer(ch)

    for v := range ch { // 通道关闭后循环自动结束
        fmt.Println("收到:", v)
    }
}

输出依次是 收到: 1、收到: 2、收到: 3。range ch 会一直接收,直到通道被 close,这个模式在"生产者-消费者"场景里非常好用。

新手最容易踩的坑

  • main 提前退出:忘了等待,goroutine 还没跑完程序就结束了。用 WaitGroup 或 channel 收尾。
  • 循环变量捕获:旧版本 Go 中直接 go func(){ 用到 i }() 可能共享同一个 i,保险起见把 i 作为参数传入。
  • 数据竞争:多个 goroutine 同时读写同一个变量没有同步,结果不可复现。能传 channel 就传 channel,实在要用共享变量再加 sync.Mutex。

小结

  • goroutine 是 Go 运行时调度的轻量并发单元,函数调用前加 go 即可启动。
  • main 返回会终止一切,等待任务用 sync.WaitGroup:Add 在启动前、Done 配合 defer、Wait 阻塞收尾。
  • goroutine 之间优先用 channel 传数据,range ch + close(ch) 是标准收发模式。
  • 并发不等于安全,多个 goroutine 写同一变量需要 channel 或 sync.Mutex 保护。