Go初级之七:并发与Goroutine” 是 Go 语言学习系列教程中的第七讲,主要介绍 Go 语言中非常核心且强大的特性——并发编程(Concurrency) 和 Goroutine。以下是该主题的详细讲解内容,适合初学者理解。
🌟 一、什么是并发(Concurrency)?
并发是指多个任务在同一时间段内交替执行(不一定是同时),它强调的是程序的结构设计,能够处理多个任务的逻辑。
Go 语言原生支持并发,通过 Goroutine 和 Channel 实现,语法简洁、性能高效。
⚠️ 注意:并发(concurrency) ≠ 并行(parallelism)
- 并发:多个任务交替执行,可能在单核上完成
- 并行:多个任务同时执行,通常需要多核支持
🚀 二、Goroutine 是什么?
Goroutine 是 Go 运行时管理的轻量级线程,由 Go Runtime 调度,开销极小。
- 启动一个 Goroutine 只需要在函数调用前加
go
关键字 - 比操作系统线程更轻量(初始栈仅 2KB)
- 数量可以成千上万,不会导致系统崩溃
✅ 示例:启动一个 Goroutine
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello from Goroutine!")
}
func main() {
go sayHello() // 启动一个 Goroutine
time.Sleep(100 * time.Millisecond) // 主协程等待,否则程序可能提前退出
fmt.Println("Main function")
}
🔍 输出可能是:
Hello from Goroutine!
Main function
也可能是:
Main function
Hello from Goroutine!
因为 Goroutine 是异步执行的。
⏳ 三、主 Goroutine 与子 Goroutine
main()
函数运行在 主 Goroutine 中- 当主 Goroutine 结束时,所有其他 Goroutine 都会被强制终止
- 所以要确保主 Goroutine 不要过早退出
❌ 错误示例(子 Goroutine 来不及执行)
func main() {
go sayHello()
// 没有等待,主 Goroutine 立即结束
}
✅ 正确做法:使用 time.Sleep
或 sync.WaitGroup
方法一:使用 time.Sleep
(不推荐,仅用于演示)
time.Sleep(1 * time.Second)
方法二:使用 sync.WaitGroup
(推荐)
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func worker(id int) {
defer wg.Done() // 任务完成,通知 WaitGroup
fmt.Printf("Worker %d starting\n", id)
time.Sleep(1 * time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
for i := 1; i <= 3; i++ {
wg.Add(1) // 增加计数器
go worker(i) // 启动 Goroutine
}
wg.Wait() // 等待所有 Goroutine 完成
fmt.Println("All workers done.")
}
✅ 输出:
Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 1 done
Worker 2 done
Worker 3 done
All workers done.
🔗 四、Goroutine 与闭包的陷阱
在 for
循环中启动 Goroutine 时,要注意变量捕获问题。
❌ 常见错误:共享变量
func main() {
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // 所有 Goroutine 都打印 3
}()
}
time.Sleep(1 * time.Second)
}
❗ 原因:
i
是同一个变量,被所有闭包共享
✅ 正确做法:传参或局部变量
for i := 0; i < 3; i++ {
go func(num int) {
fmt.Println(num)
}(i) // 立即传值
}
或:
for i := 0; i < 3; i++ {
i := i // 创建局部副本
go func() {
fmt.Println(i)
}()
}
🧵 五、Goroutine 的适用场景
- 处理大量并发请求(如 Web 服务器)
- 并行计算(如数据处理)
- 定时任务
- 监听事件(如信号、网络连接)
示例:并发请求多个 URL
package main
import (
"fmt"
"net/http"
"sync"
)
func fetch(url string, wg *sync.WaitGroup) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Error fetching %s: %v\n", url, err)
return
}
defer resp.Body.Close()
fmt.Printf("Fetched %s -> Status: %s\n", url, resp.Status)
}
func main() {
urls := []string{
"https://httpbin.org/delay/1",
"https://httpbin.org/status/200",
"https://httpbin.org/json",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go fetch(url, &wg)
}
wg.Wait()
fmt.Println("All requests completed.")
}
🧠 六、Goroutine 的注意事项
问题 | 建议 |
---|---|
主 Goroutine 提前退出 | 使用 sync.WaitGroup 等待 |
数据竞争(Data Race) | 使用 mutex 或 channel 同步 |
大量 Goroutine 泄露 | 使用 context 控制生命周期 |
调试困难 | 使用 go run -race 检测竞态条件 |
✅ 七、小结
特性 | 说明 |
---|---|
go 关键字 |
启动一个 Goroutine |
轻量 | 每个 Goroutine 初始栈小,可创建成千上万 |
调度器 | Go Runtime 自动调度,无需手动管理线程 |
协作式 + 抢占式 | 高效利用 CPU |
配合 channel | 实现安全的通信与同步 |
📚 下一讲预告:Go初级之八:Channel 与并发通信
我们将学习:
- 什么是 Channel
- 如何用 Channel 在 Goroutine 之间通信
- 缓冲 Channel、关闭 Channel
select
语句处理多个 Channel
📝 练习题(巩固理解)
- 写一个程序,启动 5 个 Goroutine,每个打印自己的编号(1~5),并等待全部完成。
- 修改上面的程序,让每个 Goroutine 执行一个耗时任务(如 sleep 500ms)。
- 尝试不用
WaitGroup
,只用time.Sleep
,观察结果是否稳定。 - 在
for
循环中启动 Goroutine 打印i
,故意制造闭包问题并修复。