G09第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

goroutine 与生命周期

goroutine 与生命周期 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这课启动第一个 goroutine:做一个定期清理的后台任务,能用 WaitGroup 等它结束,能通过停止信号让它退出,最终在服务关闭时确认没有残留的活。

问题场景

书签数据会积累,你想要一个后台任务定期清理过期草稿。Go 里 go f() 一行就能启动——但「怎么停」才是重点:服务收到 Ctrl+C 要退出时,如果后台 goroutine 还拿着存储句柄在跑,轻则日志报错,重则数据写一半。goroutine 的生命必须和服务同呼吸。

核心解释

启动和结束必须成对设计。带停止信号的后台任务:

func startJanitor(ctx context.Context, wg *sync.WaitGroup, store *MemoryStore, every time.Duration) {
	wg.Add(1) // 启动前登记,必须先于 go 语句
	go func() {
		defer wg.Done() // 退出时销账,任何退出路径都会执行
		ticker := time.NewTicker(every)
		defer ticker.Stop()
		for {
			select {
			case <-ctx.Done(): // 收到停止信号,干净退出
				log.Println("janitor stopped")
				return
			case <-ticker.C:
				if n := store.CleanExpired(); n > 0 {
					log.Printf("janitor cleaned %d", n)
				}
			}
		}
	}()
}

四个零件各司其职:wg.Add(1) 在 go 之前登记,defer wg.Done() 保证任何退出路径都销账;ticker 负责「每隔一段时间一次」,比手写 sleep 循环好在中断即时、不漂移;select 同时监听两个 channel,ctx.Done() 关闭时走退出分支;ctx 是唯一停止通道,不引入第二个布尔标志位——两套停止机制迟早打架。

main 里组装启动与等待:

func main() {
	store := NewMemoryStore()
	var wg sync.WaitGroup

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	startJanitor(ctx, &wg, store, 200*time.Millisecond)

	log.Printf("goroutines after start: %d", runtime.NumGoroutine())
	time.Sleep(time.Second) // 演示用:代替真实服务运行
	cancel()
	wg.Wait() // 阻塞直到 janitor 的 Done 被调用
	log.Printf("goroutines after stop: %d", runtime.NumGoroutine())
}

wg.Wait() 返回那一刻,你能断言后台工作全部结束——这就是「没有无法停止的工作」的证据。runtime.NumGoroutine 前后各打一次,数量回落到接近启动前,是肉眼可查的旁证。反面模式是「忘了能停」:goroutine 里只有 for 加 sleep,不监听任何停止信号,进程退出靠硬杀,定时任务正在写的数据可能停在半路。

示例拆解

go run .
# goroutines after start: 2
# janitor cleaned 3        (每 200ms 一次)
# janitor stopped          (cancel 生效)
# goroutines after stop: 1

启动后比启动前多 1 个 goroutine(janitor 自己),停止后回落——数字对得上,说明没有泄漏。实际服务里 G14 会把 ctx 换成 signal.NotifyContext 的产物,Ctrl+C 自动触发取消,janitor 这套代码一字不改。另外注意:wait 之后打日志的那行,如果换成打印 map 或写文件,就说明你已经在依赖「Wait 保证的先后关系」,这正是 WaitGroup 的价值所在。

动手任务

  1. 给 janitor 加「启动立即执行一次」的逻辑(进循环前先清理一遍),避免服务刚启动要等一个周期。
  2. 故意删掉 wg.Done() 那行,观察 wg.Wait() 永远阻塞——理解登记与销账必须对称,再改回来。
  3. 连续 cancel 五次再重启任务五次,用 runtime.NumGoroutine 确认数量不随次数增长。

常见错误

  • wg.Add(1) 写在 goroutine 函数体里:主流程可能先 Wait、goroutine 还没来得及 Add,登记必须在 go 语句之前。
  • 用 time.Sleep 猜测后台任务结束:等待靠 WaitGroup,不靠猜时长。
  • goroutine 里 panic 没人接:整个进程崩掉,长驻任务要保证逻辑不 panic 或有 recover。
  • 停止信号用布尔变量加锁实现:忘加锁就是数据竞争,ctx.Done() 的 channel 语义天生并发安全。

检查与复盘

完成下方检查题。自查清单:

  • 每个后台 goroutine 都有明确的启动登记(Add)和退出销账(Done)
  • 停止通道唯一:只有 ctx.Done(),没有第二套布尔开关
  • 服务关闭路径上有 wg.Wait(),退出前确认无残留工作

参考资料

检查题共 1 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1单选简单

关于 goroutine 的生命周期,下列说法正确的是?

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...