讲解

goroutine 是 Go 的轻量级线程:在函数调用前加一个 go 关键字,这个函数就在新的 goroutine 里并发执行,主流程不等它、继续往下走。goroutine 由 Go 运行时调度,初始栈只有几 KB,一个程序轻松起几万个——相比之下操作系统线程动辄 MB 级栈、上千个就吃力。「并发是 Go 的一等公民」,说的就是 goroutine 的廉价与易用。

主 goroutine(main 函数)退出时,所有其他 goroutine 会被直接「杀死」,不管干完没有。所以启动 goroutine 后必须有等待机制,最常用的就是 sync.WaitGroup:Add(1) 登记一个任务,任务完成时 Done(),主流程 Wait() 阻塞到计数归零。约定是 Add 在启动 goroutine 之前调用,Done 用 defer 保证必达。

多个 goroutine 读写同一份数据会产生「数据竞争」——结果不可预期,而且平时可能完全看不出来。纪律只有一条:要么不共享(各自操作各自的数据),要么共享时用 channel 或锁来保护。go run -race 或 go test -race 开启竞争检测器,能在开发和测试阶段把竞争抓出来,是并发代码的必做检查。

示例

启动 goroutine 并用 WaitGroup 等待全部完成:

package main

import (
	"fmt"
	"sync"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done() // 完成时登记,defer 保证必达
	fmt.Printf("工人 %d 开始干活\n", id)
}

func main() {
	var wg sync.WaitGroup
	for i := 1; i <= 3; i++ {
		wg.Add(1) // 启动前登记
		go worker(i, &wg)
	}
	wg.Wait() // 阻塞直到所有工人完工
	fmt.Println("全部完成")
}

并发计算:每个 goroutine 写自己的下标,互不共享即无竞争:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	results := make([]int, 5)
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			results[i] = i * i // Go 1.22 起循环变量每次迭代独立
		}()
	}
	wg.Wait()
	fmt.Println("平方结果:", results)
}

常见坑

  • main 提前返回,goroutine 被秒杀:起了 goroutine 就结束 main,子任务可能一行都没执行。用 WaitGroup 或 channel 同步,不要靠 time.Sleep 赌时间。
  • WaitGroup 计数失衡:Add 的次数和 Done 的次数不一致,要么 Wait 永不返回(死锁),要么提前放行。defer wg.Done() 是最稳的配对方式。
  • 并发写同一变量:两个 goroutine 同时 counter++ 不是原子操作,结果会少加。用 -race 跑测试能抓出来;修法是 channel、sync.Mutex 或 atomic。
  • 把 WaitGroup 按值拷贝:wg 必须传指针(或放结构体里传指针),值拷贝会让 Done 减的不是同一个计数器。go vet 会检查这类拷贝。
  • 以为 goroutine 数量无上限:goroutine 虽廉价但不是免费,每个都吃内存,失控创建(比如对每个请求无限制地起)照样撑爆内存。高并发场景用固定数量的 worker 池。

小结

go 关键字起并发,WaitGroup 等待收工,共享数据要同步,-race 抓竞争。下一节学习 goroutine 之间通信的正道:channel。