Go 从 0 到精通 · 第 25 课:goroutine 入门
学习定位:这是整套 Go 教程的第 25 课,也是阶段五(并发与工程阶段)的第一课。
前置要求:已经完成第 24 课,具备用 Go 编写完整小项目的能力。需要熟悉函数、切片、循环等基础知识。
本课目标:理解 Go 并发的基本概念,能创建和运行 goroutine,理解主协程退出问题并掌握基本的等待方式,能初步感受并发编程的威力和复杂性。
1. 本课你要解决的核心问题
到目前为止,你写的所有程序都是顺序执行 的——一行一行往下跑,做完一件事才做下一件事。但现实世界不是这样运转的:
一个 Web 服务器要同时 处理多个用户的请求
一个下载器要同时 下载多个文件
一个聊天程序要同时 发送和接收消息
这就是并发 要解决的问题。而 Go 语言最大的卖点之一,就是它把并发做得极其简单 。
你需要搞明白以下问题:
什么是并发,和并行有什么区别
goroutine 是什么
怎么创建一个 goroutine
为什么我的 goroutine “没有执行”
怎么等 goroutine 执行完
goroutine 和线程有什么区别
开多少个 goroutine 合适
学完这一课,你就迈进了 Go 最强大也最有意思的领域。
2. 先搞清楚概念:并发 vs 并行
这两个词经常被混用,但它们不是一回事。
2.1 并发(Concurrency)
并发是一种程序设计方式 ——把任务拆成可以独立推进的部分。
想象一个厨师做饭:先把水烧上,等水开的时候去切菜,切完菜再去下面条。一个人,但三件事在"同时"推进。这就是并发——同一时间段内处理多件事 。
2.2 并行(Parallelism)
并行是真正的同时执行 ——多个 CPU 核心各干各的。
两个厨师,一个切菜,一个烧水。两件事在同一时刻 同时发生。这才是并行。
2.3 关系
1 2 并发:一个人同时管几件事(交替推进) 并行:多个人同时干活(真正同时)
Go 的 goroutine 是并发 的设计工具。如果你的机器有多个 CPU 核心,Go 的运行时会自动把 goroutine 分配到不同核心上并行执行 。但你写代码时,只需要关心并发设计——怎么拆任务,Go 的运行时帮你搞定并行。
3. goroutine 是什么
3.1 一句话定义
goroutine 是 Go 中的轻量级执行单元 。你可以把它理解为一个超轻量的"线程"。
3.2 你已经用过一个 goroutine 了
每个 Go 程序启动时,main 函数就运行在一个 goroutine 里——叫做主 goroutine 。之前写的所有程序都是只有这一个 goroutine。
1 2 3 func main () { fmt.Println("我在主 goroutine 中运行" ) }
这一课要学的就是:怎么创建更多的 goroutine。
4. 创建你的第一个 goroutine
4.1 go 关键字
创建 goroutine 只需要一个关键字:go 。在函数调用前面加 go,这个函数就会在一个新的 goroutine 中执行:
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" func sayHello () { fmt.Println("Hello from goroutine!" ) } func main () { go sayHello() fmt.Println("Hello from main!" ) }
运行一下:
等等,sayHello 的输出呢?
4.2 第一个大坑:主 goroutine 退出,程序就结束了
这是学 goroutine 必须搞懂的第一件事:
当 main 函数返回时,程序立即结束,不管其他 goroutine 有没有执行完。
上面的代码中,go sayHello() 启动了一个新的 goroutine,但 main 函数紧接着就执行了 fmt.Println("Hello from main!"),然后 main 返回,程序结束。新的 goroutine 还没来得及执行就被"杀"了。
这就像一个公司的老板(主 goroutine)下班走人了,员工(其他 goroutine)不管手里的活干没干完,公司直接关门。
4.3 临时方案:用 time.Sleep 等一等
最粗暴的办法是让主 goroutine 睡一会儿,给其他 goroutine 时间执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" "time" ) func sayHello () { fmt.Println("Hello from goroutine!" ) } func main () { go sayHello() time.Sleep(100 * time.Millisecond) fmt.Println("Hello from main!" ) }
输出:
1 2 Hello from goroutine! Hello from main !
这次看到 goroutine 的输出了。但这是一个很烂的解决方案 :
你不知道 goroutine 需要多少时间才能执行完
睡少了——goroutine 还没执行完
睡多了——白白浪费时间
生产环境绝对不能用这种方式
但是作为学习工具,time.Sleep 能帮你直观地看到 goroutine 确实在运行。后面我们马上学正确的等待方式。
5. 启动多个 goroutine
5.1 看看多个 goroutine 交替执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "fmt" "time" ) func printNumbers (name string ) { for i := 1 ; i <= 5 ; i++ { fmt.Printf("[%s] %d\n" , name, i) time.Sleep(50 * time.Millisecond) } } func main () { go printNumbers("A" ) go printNumbers("B" ) go printNumbers("C" ) time.Sleep(500 * time.Millisecond) fmt.Println("全部完成" ) }
可能的输出(每次运行可能不同):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [C] 1 [A] 1 [B] 1 [A] 2 [C] 2 [B] 2 [B] 3 [C] 3 [A] 3 [A] 4 [B] 4 [C] 4 [C] 5 [A] 5 [B] 5 全部完成
关键观察 :
A、B、C 的输出是交错的 ——它们在并发执行
每次运行的顺序可能不同 ——这是并发的本质特征
三个任务本来需要 3 × 250ms = 750ms,但并发执行只用了约 250ms
5.2 顺序执行 vs 并发执行的对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( "fmt" "time" ) func task (name string ) { fmt.Printf("%s 开始\n" , name) time.Sleep(200 * time.Millisecond) fmt.Printf("%s 结束\n" , name) } func main () { fmt.Println("=== 顺序执行 ===" ) start := time.Now() task("任务1" ) task("任务2" ) task("任务3" ) fmt.Printf("耗时: %v\n\n" , time.Since(start)) fmt.Println("=== 并发执行 ===" ) start = time.Now() go task("任务1" ) go task("任务2" ) go task("任务3" ) time.Sleep(300 * time.Millisecond) fmt.Printf("耗时: %v\n" , time.Since(start)) }
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 === 顺序执行 === 任务1 开始 任务1 结束 任务2 开始 任务2 结束 任务3 开始 任务3 结束 耗时: 600ms === 并发执行 === 任务3 开始 任务1 开始 任务2 开始 任务1 结束 任务2 结束 任务3 结束 耗时: 300ms
顺序执行 600ms,并发执行约 300ms——快了一倍 。这就是并发的价值:多件事同时推进,总时间取决于最慢的那一个。
6. 正确的等待方式:sync.WaitGroup
time.Sleep 是猜时间,不靠谱。Go 标准库提供了 sync.WaitGroup,让你精确地等待一组 goroutine 执行完毕。
6.1 基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport ( "fmt" "sync" "time" ) func worker (id int , wg *sync.WaitGroup) { defer wg.Done() fmt.Printf("Worker %d 开始\n" , id) time.Sleep(100 * time.Millisecond) fmt.Printf("Worker %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("所有 Worker 执行完毕" ) }
输出:
1 2 3 4 5 6 7 Worker 3 开始 Worker 1 开始 Worker 2 开始 Worker 1 完成 Worker 3 完成 Worker 2 完成 所有 Worker 执行完毕
6.2 WaitGroup 三步曲
sync.WaitGroup 就三个方法,像一个计数器:
方法
作用
什么时候调用
wg.Add(n)
计数器加 n
启动 goroutine 之前
wg.Done()
计数器减 1
goroutine 结束时
wg.Wait()
阻塞等待,直到计数器变为 0
主 goroutine 中
工作流程:
1 2 3 4 5 6 7 8 Add (1 ) → 计数器: 1 启动第 1 个 goroutineAdd (1 ) → 计数器: 2 启动第 2 个 goroutineAdd (1 ) → 计数器: 3 启动第 3 个 goroutineWait () → 阻塞...Done () → 计数器: 2 第 X 个 goroutine 完成Done () → 计数器: 1 第 Y 个 goroutine 完成Done () → 计数器: 0 第 Z 个 goroutine 完成Wait () → 解除阻塞 继续执行
6.3 defer wg.Done() 的重要性
1 2 3 4 5 6 7 8 9 10 11 func worker (id int , wg *sync.WaitGroup) { defer wg.Done() if id == 2 { fmt.Printf("Worker %d 跳过\n" , id) return } fmt.Printf("Worker %d 工作中\n" , id) }
为什么用 defer (第 17 课):如果函数有多个 return 路径,或者中间可能 panic,用 defer wg.Done() 保证计数器一定会减 1。如果忘了调 Done(),Wait() 会永远阻塞,程序就卡死了。
6.4 WaitGroup 必须传指针
1 2 3 4 5 6 7 8 9 10 11 func worker (id int , wg sync.WaitGroup) { defer wg.Done() } func worker (id int , wg *sync.WaitGroup) { defer wg.Done() }
sync.WaitGroup 是一个结构体,传值会拷贝一份(第 12 课)。你在副本上调 Done(),原始的计数器不会变,Wait() 永远等不到。
7. 匿名函数 goroutine
7.1 基本写法
不是每个 goroutine 都需要单独定义一个函数。用匿名函数更简洁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" "sync" ) func main () { var wg sync.WaitGroup for i := 1 ; i <= 3 ; i++ { wg.Add(1 ) go func (id int ) { defer wg.Done() fmt.Printf("goroutine %d 执行\n" , id) }(i) } wg.Wait() fmt.Println("完成" ) }
输出(顺序不确定):
1 2 3 4 goroutine 3 执行 goroutine 1 执行 goroutine 2 执行 完成
7.2 第二个大坑:闭包变量捕获
这是 Go 并发编程中最经典的 bug 。看看不传参数会怎样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "sync" ) func main () { var wg sync.WaitGroup for i := 1 ; i <= 3 ; i++ { wg.Add(1 ) go func () { defer wg.Done() fmt.Printf("goroutine %d 执行\n" , i) }() } wg.Wait() }
你可能期望输出 1、2、3,但实际输出很可能是:
1 2 3 goroutine 4 执行goroutine 4 执行goroutine 4 执行
为什么全是 4?
for 循环执行得非常快,三个 goroutine 被创建出来了,但还没来得及运行
循环结束时 i 的值已经变成了 4(i++ 之后不满足 i <= 3,退出循环)
三个 goroutine 开始执行,它们都去读 i 的值——此时 i 已经是 4 了
三个 goroutine 共享同一个变量 i ,这就是闭包的特性
解决办法 :把变量作为参数传进去,每个 goroutine 拿到的是一份独立的拷贝。
1 2 3 4 5 6 7 8 9 10 11 12 go func (id int ) { fmt.Printf("goroutine %d 执行\n" , id) }(i) for i := 1 ; i <= 3 ; i++ { id := i go func () { fmt.Printf("goroutine %d 执行\n" , id) }() }
两种都可以,第一种更明确。
8. goroutine 有多轻量
8.1 创建大量 goroutine
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "sync" "time" ) func main () { count := 100000 var wg sync.WaitGroup start := time.Now() for i := 0 ; i < count; i++ { wg.Add(1 ) go func (id int ) { defer wg.Done() _ = id * id }(i) } wg.Wait() fmt.Printf("创建并完成 %d 个 goroutine,耗时: %v\n" , count, time.Since(start)) }
在普通电脑上,十万个 goroutine 通常在几百毫秒内就能创建并完成。
8.2 goroutine vs 线程
特性
goroutine
操作系统线程
初始栈大小
约 2KB
约 1MB
创建开销
极低(微秒级)
较高(毫秒级)
能创建的数量
轻松十万级
通常几千就很多了
调度方式
Go 运行时调度(用户态)
操作系统调度(内核态)
切换开销
很低
较高
关键区别 :goroutine 不是线程。Go 运行时会把成千上万个 goroutine 映射到少量的操作系统线程上(默认等于 CPU 核心数)。goroutine 的调度完全在用户态完成,不需要进入操作系统内核,所以非常快。
8.3 goroutine 的栈是动态增长的
1 2 3 4 5 6 7 8 操作系统线程: 创建时分配 1 MB 栈空间(固定大小) 1 万个线程 = 10 GB 内存 goroutine: 创建时只分配约 2 KB 栈空间 需要更多时自动增长(最大 1 GB) 1 万个 goroutine ≈ 20 MB 内存
这就是为什么 Go 能轻松创建大量 goroutine——内存占用非常小。
9. goroutine 的执行顺序
9.1 顺序不确定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "sync" ) func main () { var wg sync.WaitGroup for i := 1 ; i <= 5 ; i++ { wg.Add(1 ) go func (id int ) { defer wg.Done() fmt.Println(id) }(i) } wg.Wait() }
运行多次,你会看到不同的输出顺序:
1 2 3 第一次: 5 3 1 2 4 第二次: 1 5 2 4 3 第三次: 3 1 5 4 2
并发程序的执行顺序是不确定的 。如果你的程序依赖 goroutine 的执行顺序,那说明设计有问题。goroutine 之间的协调要通过同步机制(WaitGroup、channel 等),不能靠"碰巧的执行顺序"。
9.2 不能假设先启动的先执行
1 2 3 4 5 6 go funcA() go funcB()
10. 有返回值的函数怎么办
goroutine 启动的函数不能直接获取返回值 :
1 2 result := go computeSomething()
这是因为 go 启动的函数是异步执行的,调用方不会等它完成,自然也没法立刻拿到返回值。
那怎么拿到 goroutine 的执行结果? 目前可以用共享变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "fmt" "sync" ) func main () { var wg sync.WaitGroup results := make ([]int , 5 ) for i := 0 ; i < 5 ; i++ { wg.Add(1 ) go func (idx int ) { defer wg.Done() results[idx] = idx * idx }(i) } wg.Wait() fmt.Println(results) }
这里能正常工作是因为每个 goroutine 写的是 results 中不同的索引位置,不存在竞争。
但如果多个 goroutine 要写同一个变量 ,就有问题了——这叫数据竞争 (data race)。下一课会讲用 channel 来安全地传递数据,第 28 课会讲用 Mutex 来保护共享数据。
11. 实战示例
11.1 并发下载模拟器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 package mainimport ( "fmt" "math/rand" "sync" "time" ) type DownloadResult struct { URL string Size int Duration time.Duration Err error } func download (url string ) DownloadResult { start := time.Now() duration := time.Duration(100 +rand.Intn(400 )) * time.Millisecond time.Sleep(duration) size := 1024 + rand.Intn(9216 ) return DownloadResult{ URL: url, Size: size, Duration: time.Since(start), } } func main () { urls := []string { "https://example.com/file1.txt" , "https://example.com/file2.txt" , "https://example.com/file3.txt" , "https://example.com/file4.txt" , "https://example.com/file5.txt" , } fmt.Println("=== 顺序下载 ===" ) start := time.Now() for _, url := range urls { result := download(url) fmt.Printf(" %s: %dKB, 耗时 %v\n" , result.URL, result.Size/1024 , result.Duration) } seqDuration := time.Since(start) fmt.Printf("顺序下载总耗时: %v\n\n" , seqDuration) fmt.Println("=== 并发下载 ===" ) start = time.Now() var wg sync.WaitGroup results := make ([]DownloadResult, len (urls)) for i, url := range urls { wg.Add(1 ) go func (idx int , u string ) { defer wg.Done() results[idx] = download(u) }(i, url) } wg.Wait() for _, result := range results { fmt.Printf(" %s: %dKB, 耗时 %v\n" , result.URL, result.Size/1024 , result.Duration) } concDuration := time.Since(start) fmt.Printf("并发下载总耗时: %v\n" , concDuration) fmt.Printf("加速比: %.1fx\n" , float64 (seqDuration)/float64 (concDuration)) }
可能的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 === 顺序下载 === file1.txt: 5KB, 耗时 347ms file2.txt: 2KB, 耗时 156ms file3.txt: 8KB, 耗时 423ms file4.txt: 3KB, 耗时 201ms file5.txt: 6KB, 耗时 389ms 顺序下载总耗时: 1.516s === 并发下载 === file1.txt: 4KB, 耗时 289ms file2.txt: 7KB, 耗时 412ms file3.txt: 1KB, 耗时 134ms file4.txt: 5KB, 耗时 367ms file5.txt: 3KB, 耗时 198ms 并发下载总耗时: 412ms 加速比: 3.7x
顺序下载需要所有时间加起来,并发下载只需要最慢那个的时间。5 个任务并发执行,获得了约 3~4 倍的加速。
11.2 并发计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package mainimport ( "fmt" "sync" "time" ) func sumRange (start, end int ) int { sum := 0 for i := start; i <= end; i++ { sum += i } return sum } func main () { total := 1_000_000_000 start := time.Now() result := sumRange(1 , total) fmt.Printf("单 goroutine: 结果=%d, 耗时=%v\n" , result, time.Since(start)) start = time.Now() numWorkers := 4 chunkSize := total / numWorkers partialResults := make ([]int , numWorkers) var wg sync.WaitGroup for i := 0 ; i < numWorkers; i++ { wg.Add(1 ) go func (idx int ) { defer wg.Done() s := idx*chunkSize + 1 e := (idx + 1 ) * chunkSize if idx == numWorkers-1 { e = total } partialResults[idx] = sumRange(s, e) }(i) } wg.Wait() finalResult := 0 for _, r := range partialResults { finalResult += r } fmt.Printf("4 goroutine: 结果=%d, 耗时=%v\n" , finalResult, time.Since(start)) }
这个例子展示了分治思想 :把一个大计算拆成 4 段,每段一个 goroutine 并行计算,最后汇总。在多核机器上,你能看到明显的加速。
11.3 并发执行多个独立任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" "sync" "time" ) func main () { var wg sync.WaitGroup tasks := map [string ]time.Duration{ "加载配置" : 200 * time.Millisecond, "连接数据库" : 300 * time.Millisecond, "初始化缓存" : 150 * time.Millisecond, "加载模板" : 100 * time.Millisecond, } start := time.Now() for name, duration := range tasks { wg.Add(1 ) go func (taskName string , taskDuration time.Duration) { defer wg.Done() fmt.Printf("[开始] %s\n" , taskName) time.Sleep(taskDuration) fmt.Printf("[完成] %s (耗时 %v)\n" , taskName, taskDuration) }(name, duration) } wg.Wait() fmt.Printf("\n所有初始化完成,总耗时: %v\n" , time.Since(start)) }
可能的输出:
1 2 3 4 5 6 7 8 9 10 [开始] 加载模板[开始] 加载配置[开始] 连接数据库[开始] 初始化缓存[完成] 加载模板 (耗时 100ms )[完成] 初始化缓存 (耗时 150ms )[完成] 加载配置 (耗时 200ms )[完成] 连接数据库 (耗时 300ms )所有初始化完成,总耗时: 301ms
四个任务如果顺序执行需要 750ms,并发执行只需要 300ms(取决于最慢的任务)。这在 Web 服务的启动阶段很常见——多个初始化任务互不依赖,可以并行完成。
12. 常见坑总结
12.1 忘了等 goroutine 完成
1 2 3 4 func main () { go doSomething() }
解决 :用 sync.WaitGroup 等待。
12.2 WaitGroup 传值而不是传指针
1 2 3 4 5 6 7 8 9 func worker (wg sync.WaitGroup) { defer wg.Done() } func worker (wg *sync.WaitGroup) { defer wg.Done() }
12.3 Add 和 go 的顺序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for i := 0 ; i < 3 ; i++ { go func () { wg.Add(1 ) defer wg.Done() }() } wg.Wait() for i := 0 ; i < 3 ; i++ { wg.Add(1 ) go func () { defer wg.Done() }() } wg.Wait()
wg.Add(1) 必须在 go 之前调用。如果在 goroutine 内部调用,可能还没来得及 Add,主 goroutine 就已经 Wait 了(此时计数器为 0,Wait 直接返回)。
12.4 闭包捕获循环变量
1 2 3 4 5 6 7 8 9 10 11 12 13 for i := 0 ; i < 3 ; i++ { go func () { fmt.Println(i) }() } for i := 0 ; i < 3 ; i++ { go func (id int ) { fmt.Println(id) }(i) }
12.5 在 goroutine 里 panic
1 2 3 4 5 6 func main () { go func () { panic ("goroutine 挂了" ) }() time.Sleep(time.Second) }
goroutine 里的 panic 会导致整个程序崩溃 ,不仅仅是那一个 goroutine。如果需要防止这种情况,要在 goroutine 内部自己 recover(第 17 课):
1 2 3 4 5 6 7 8 go func () { defer func () { if r := recover (); r != nil { fmt.Printf("goroutine 恢复: %v\n" , r) } }() panic ("出错了" ) }()
12.6 goroutine 泄漏
1 2 3 4 5 6 7 8 9 10 func leak () { go func () { for { time.Sleep(time.Second) } }() }
如果不断调用 leak(),每次都会创建一个永远不会结束的 goroutine,内存会持续增长。这就是 goroutine 泄漏 。后面第 29 课会讲用 context 来控制 goroutine 的生命周期。
13. 本课练习
练习 1:并发计时器
要求:
启动 3 个 goroutine
每个 goroutine 每隔 500ms 打印一次自己的名字和当前时间
每个 goroutine 打印 5 次后结束
主 goroutine 等所有 goroutine 完成后打印"全部完成"
练习 2:并发求和
要求:
给定一个有 100 个元素的 int 切片
把它分成 10 段,每段 10 个元素
启动 10 个 goroutine 分别计算每段的和
主 goroutine 汇总结果并输出总和
练习 3:并发文件处理
要求:
创建 5 个文本文件(file1.txt ~ file5.txt),每个文件写入一些文本
启动 5 个 goroutine 同时读取这 5 个文件
每个 goroutine 统计文件中的字符数
主 goroutine 汇总并输出总字符数
提示:用第 19 课学的文件操作配合 goroutine。
练习 4:感受闭包陷阱
要求:
写一段代码,故意触发闭包捕获循环变量的 bug,观察输出
然后用两种方式修复它(传参数、创建局部变量),对比输出
练习 5:goroutine 数量实验
要求:
写一个程序,分别创建 100、1000、10000、100000 个 goroutine
每个 goroutine 做一个简单操作(比如把数字加到结果中)
记录并打印每种数量下的总耗时
观察 goroutine 数量和耗时的关系
14. 自测题
14.1 概念题
go 关键字的作用是什么?
主 goroutine 退出后,其他 goroutine 会怎样?
sync.WaitGroup 的三个方法分别是什么?各在什么时候调用?
为什么 WaitGroup 作为函数参数时必须传指针?
为什么 wg.Add(1) 必须在 go 语句之前而不是在 goroutine 内部?
goroutine 和操作系统线程有什么区别?
什么是闭包变量捕获问题?怎么解决?
goroutine 中发生 panic 会怎样?
14.2 代码阅读题
预测以下代码的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" "sync" ) func main () { var wg sync.WaitGroup for i := 0 ; i < 3 ; i++ { wg.Add(1 ) go func (n int ) { defer wg.Done() fmt.Printf("%d " , n*n) }(i) } wg.Wait() fmt.Println("done" ) }
点击查看答案
其中 X 是 0、1、4 的某种排列(比如 4 0 1 done 或 0 1 4 done)。
解释:
循环启动 3 个 goroutine,分别传入 i=0、1、2
每个 goroutine 计算 n*n 并打印:0*0=0、1*1=1、2*2=4
三个 goroutine 的执行顺序不确定 ,所以 0、1、4 可能以任意顺序出现
wg.Wait() 保证三个 goroutine 都完成后才打印 “done”
“done” 一定是最后输出的
15. 本课总结
这一课你学到了 Go 并发编程的基础。
场景
做法
创建 goroutine
函数调用前加 go
等待 goroutine 完成
sync.WaitGroup
goroutine 中用循环变量
作为参数传入,避免闭包陷阱
goroutine 中防 panic
在 goroutine 内部 defer recover
获取 goroutine 的结果
预分配结果切片,按索引写入
最重要的三件事:
go 关键字创建 goroutine,但主 goroutine 退出就全完了——必须用 WaitGroup 等待
goroutine 的执行顺序不确定——不要依赖顺序,要用同步机制协调
闭包捕获循环变量是经典 bug——永远把循环变量作为参数传给 goroutine
16. 下一课预告
这一课你学会了启动多个 goroutine 并行工作。但你有没有发现一个问题:goroutine 之间怎么通信?
目前我们只能用共享变量传递数据,而且还很容易出问题。Go 有一句著名的格言:
“不要通过共享内存来通信,而要通过通信来共享内存。”
下一课:channel 入门
会重点讲:
channel 是什么——goroutine 之间的通信管道
无缓冲 channel 和有缓冲 channel 的区别
怎么用 channel 发送和接收数据
channel 的阻塞行为
用 range 遍历 channel
关闭 channel
学完下一课,你就能让 goroutine 之间安全、优雅地互相传递数据了。