适用对象 :已掌握 Go 基础语法(变量、函数、结构体、接口、基础错误处理)的学习者
目标 :深入理解 Go 的并发模型(goroutine + channel),掌握 sync 和 context 包,能编写高效、安全的并发程序
时间建议:5--7 天,每天 1.5--2 小时学习 + 编码练习
一、并发基础回顾:goroutine 与调度模型
1.1 什么是 goroutine?
Goroutine 是 Go 运行时管理的轻量级线程。启动一个 goroutine 只需在函数调用前加 go 关键字:
go
go func() {
fmt.Println("Hello from goroutine!")
}()
- 轻量:初始栈仅 2KB,可动态扩容。
- 由 Go runtime 调度(GMP 模型,见下文),而非操作系统线程。
- 成本极低:可轻松创建成千上万个。
💡 注意:主 goroutine(main 函数)退出后,所有子 goroutine 会被强制终止。
示例:主协程提前退出导致子协程未执行
go
package main
import (
"fmt"
"time"
)
func main() {
go func() {
fmt.Println("This may not print!")
}()
// 主函数立即结束,子 goroutine 可能未执行
}
✅ 解决方法 :使用 time.Sleep(仅用于演示)或 sync.WaitGroup(生产推荐)。
go
// 正确做法:等待子协程完成
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Now it prints!")
}()
wg.Wait() // 阻塞直到 Done 被调用
1.2 GMP 调度模型(简要理解)
Go 采用 GMP 模型实现高效并发调度:
- G (Goroutine):用户级协程。
- M (Machine/OS Thread):操作系统线程。
- P (Processor):逻辑处理器,包含 Goroutine 队列,M 必须绑定 P 才能运行 G。
⚙️ 默认 P 的数量 = CPU 核心数(可通过
GOMAXPROCS调整)。
go
import "runtime"
func main() {
fmt.Println("NumCPU:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // 0 表示不修改,仅查询
}
意义:Go 的并发调度在用户态完成,避免频繁系统调用,提升性能。
二、Channel:Go 并发通信的核心
2.1 Channel 基础
Channel 是 goroutine 之间通信的"管道",遵循 CSP(Communicating Sequential Processes) 模型。
go
ch := make(chan int) // 无缓冲 channel
ch := make(chan int, 10) // 有缓冲 channel(容量 10)
- 无缓冲 channel:发送和接收必须同时就绪(同步)。
- 有缓冲 channel:缓冲未满可发送,缓冲非空可接收(异步)。
示例:无缓冲 channel 同步通信
go
func main() {
ch := make(chan string)
go func() {
ch <- "Hello from goroutine!" // 阻塞直到有人接收
}()
msg := <-ch // 阻塞直到有人发送
fmt.Println(msg)
}
✅ 输出:
Hello from goroutine!
示例:有缓冲 channel(非阻塞发送,直到缓冲满)
go
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
// ch <- 3 // 如果取消注释,会阻塞(缓冲已满)
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
}
2.2 select 语句:多路 channel 通信
select 类似 switch,但用于 channel 操作。它会随机执行一个可执行的 case。
go
select {
case msg := <-ch1:
fmt.Println("Received from ch1:", msg)
case msg := <-ch2:
fmt.Println("Received from ch2:", msg)
case ch3 <- data:
fmt.Println("Sent to ch3")
default:
fmt.Println("No channel ready")
}
⚠️ 注意 :若无
default,select会阻塞直到某个 case 可执行。
示例:超时控制
go
func main() {
ch := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
ch <- "result"
}()
select {
case res := <-ch:
fmt.Println("Got:", res)
case <-time.After(1 * time.Second):
fmt.Println("Timeout!")
}
}
// 输出:Timeout!
2.3 并发模式实战
模式 1:Worker Pool(工作池)
用于限制并发数量,避免资源耗尽。
go
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(time.Second) // 模拟耗时
results <- job * 2
}
}
func main() {
const numJobs = 5
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// 启动 workers
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go worker(i, jobs, results, &wg)
}
// 发送任务
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// 等待所有 worker 结束
wg.Wait()
close(results)
// 收集结果
for res := range results {
fmt.Println("Result:", res)
}
}
模式 2:Pipeline(流水线)
将任务拆分为多个阶段,并行处理。
go
// 生成器
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
// 平方处理器
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
func main() {
// pipeline: gen -> sq -> print
for n := range sq(gen(2, 3, 4)) {
fmt.Println(n) // 4, 9, 16
}
}
三、sync 包:并发原语工具箱
3.1 sync.WaitGroup
等待一组 goroutine 完成。
go
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
// do work
}()
go func() {
defer wg.Done()
// do work
}()
wg.Wait() // 阻塞直到计数归零
❌ 不要复制 WaitGroup(应传指针)。
3.2 sync.Mutex 与 sync.RWMutex
- Mutex:互斥锁,保护共享资源。
- RWMutex:读写锁,允许多个读或单个写。
示例:并发安全的计数器
go
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
RWMutex 示例:缓存读多写少
go
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
3.3 sync.Once
确保某段代码只执行一次(常用于单例初始化)。
go
var once sync.Once
var db *Database
func getDB() *Database {
once.Do(func() {
db = connectToDB() // 只执行一次
})
return db
}
四、context 包:请求作用域的控制
context 用于在 goroutine 之间传递取消信号、超时、截止时间、请求元数据。
4.1 常见 context 类型
| 函数 | 作用 |
|---|---|
context.Background() |
根 context,通常用于 main 或初始化 |
context.WithCancel(parent) |
可手动取消 |
context.WithTimeout(parent, duration) |
超时自动取消 |
context.WithValue(parent, key, val) |
传递请求级数据(慎用) |
4.2 示例:超时控制 HTTP 请求
go
func fetchData(ctx context.Context) (string, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", "https://httpbin.org/delay/2", nil)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return string(body), nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
result, err := fetchData(ctx)
if err != nil {
fmt.Println("Error:", err) // context deadline exceeded
return
}
fmt.Println(result)
}
4.3 在 goroutine 中监听 context
go
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Second)
cancel() // 主动取消
}()
select {
case <-time.After(5 * time.Second):
fmt.Println("Too late!")
case <-ctx.Done():
fmt.Println("Canceled:", ctx.Err()) // context canceled
}
}
✅ 最佳实践 :所有长时间运行的 goroutine 都应监听
ctx.Done()。
五、本周练习任务
💡 建议将每个练习写成独立项目,使用
go mod init week1-ex1初始化模块。
练习 1:并发爬虫(Worker Pool)
- 创建一个爬虫,从多个 URL 并发获取 HTML 内容。
- 限制并发数为 3。
- 使用
context.WithTimeout设置整体超时(如 5 秒)。 - 打印每个 URL 的响应长度。
提示:
- 使用
http.Get获取内容。 - 用 channel 传递 URL 和结果。
- 处理错误(如网络超时)。
练习 2:带过期时间的并发安全缓存
- 实现
ExpiringCache结构体。 - 支持
Set(key string, value interface{}, ttl time.Duration) - 支持
Get(key string) (interface{}, bool) - 使用
RWMutex保证并发安全。 - 后台启动一个 goroutine 定期清理过期项(可选进阶)。
练习 3:模拟"比赛"场景
- 启动 5 个 goroutine 模拟运动员跑步。
- 每个运动员随机耗时 1--3 秒完成。
- 使用
context.WithCancel:第一名完成后,取消其余运动员。 - 打印"冠军是 #X"。
关键点:
- 使用
context传递取消信号。 - 所有 goroutine 应监听
ctx.Done()。
六、常见陷阱与调试技巧
| 陷阱 | 解决方案 |
|---|---|
| goroutine 泄漏 | 始终监听 ctx.Done() 或使用 WaitGroup |
| 死锁(deadlock) | 避免无缓冲 channel 在单 goroutine 中收发 |
| 竞态条件(race) | 使用 go run -race 检测 |
| channel 忘记关闭 | 在生产者结束时 close(ch),让消费者用 for range |
✅ 调试命令:
bash
# 检测竞态
go run -race main.go
# 查看 goroutine 堆栈(调试死锁)
kill -SIGABRT <pid>