Go 语言进阶学习:第 1 周 —— 并发编程深度掌握

适用对象 :已掌握 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")
}

⚠️ 注意 :若无 defaultselect 会阻塞直到某个 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>

七、延伸阅读

相关推荐
荒诞硬汉2 分钟前
面向对象(三)
java·开发语言
郝学胜-神的一滴3 分钟前
深入理解Linux中的Try锁机制
linux·服务器·开发语言·c++·程序人生
liliangcsdn3 分钟前
bash中awk如何切分输出
开发语言·bash
csbysj202010 分钟前
JSON.parse() 方法详解
开发语言
乐观主义现代人10 分钟前
redis 源码学习笔记
redis·笔记·学习
YJlio10 分钟前
Registry Usage (RU) 学习笔记(15.5):注册表内存占用体检与 Hive 体量分析
服务器·windows·笔记·python·学习·tcp/ip·django
奔波霸的伶俐虫12 分钟前
redisTemplate.opsForList()里面方法怎么用
java·开发语言·数据库·python·sql
yesyesido23 分钟前
智能文件格式转换器:文本/Excel与CSV无缝互转的在线工具
开发语言·python·excel
_200_25 分钟前
Lua 流程控制
开发语言·junit·lua
环黄金线HHJX.26 分钟前
拼音字母量子编程PQLAiQt架构”这一概念。结合上下文《QuantumTuan ⇆ QT:Qt》
开发语言·人工智能·qt·编辑器·量子计算