16 - Go 协程(goroutine):从基础到实战

文章目录

  • [🚀 16 - Go 协程(goroutine):从基础到实战](#🚀 16 - Go 协程(goroutine):从基础到实战)
  • [什么是 goroutine?](#什么是 goroutine?)
    • [🚀 第一个 goroutine](#🚀 第一个 goroutine)
  • [goroutine 执行机制](#goroutine 执行机制)
    • [🔥 关键模型:GMP 模型](#🔥 关键模型:GMP 模型)
    • [🧠 调度流程(简化版)](#🧠 调度流程(简化版))
    • [🚀 为什么 goroutine 很轻?](#🚀 为什么 goroutine 很轻?)
  • [goroutine + channel(核心组合)](#goroutine + channel(核心组合))
    • [📦 channel 基础](#📦 channel 基础)
    • [示例:goroutine 通信](#示例:goroutine 通信)
    • [🔁 带缓冲 channel](#🔁 带缓冲 channel)
  • [goroutine 实战场景](#goroutine 实战场景)
    • [🧪 并发任务处理](#🧪 并发任务处理)
    • [🧪 使用 WaitGroup 控制并发](#🧪 使用 WaitGroup 控制并发)
    • [🧪 并发安全(Mutex)](#🧪 并发安全(Mutex))
  • [goroutine 常见坑(必会)](#goroutine 常见坑(必会))
    • [❌ 主 goroutine 提前退出](#❌ 主 goroutine 提前退出)
    • [❌ 闭包变量问题(经典面试题)](#❌ 闭包变量问题(经典面试题))
    • [❌ goroutine 泄漏](#❌ goroutine 泄漏)
  • [goroutine 调度细节(进阶)](#goroutine 调度细节(进阶))
    • [⏱ 抢占式调度(Go 1.14+)](#⏱ 抢占式调度(Go 1.14+))
    • [🔄 调度时机](#🔄 调度时机)
  • 性能优化建议
    • [🚀 控制 goroutine 数量](#🚀 控制 goroutine 数量)
    • [🚀 使用 sync.Pool 复用对象](#🚀 使用 sync.Pool 复用对象)
    • [🚀 合理使用 channel](#🚀 合理使用 channel)
  • 总结
    • [🎯 goroutine 核心要点](#🎯 goroutine 核心要点)
    • [🎯 并发三件套](#🎯 并发三件套)
    • [🎯 一句话总结](#🎯 一句话总结)
  • [📌 面试高频问题](#📌 面试高频问题)

🚀 16 - Go 协程(goroutine):从基础到实战

Go 的并发之所以强大,不是因为它快,而是因为它"简单且优雅"。

在 Go 语言中,并发编程的核心就是 goroutine。它让你用极低的成本实现高并发,是 Go 被称为"云原生语言"的关键原因之一。


什么是 goroutine?

goroutine 是 Go 语言中的轻量级线程(用户态线程)

👉 特点:

  • 占用内存极小(初始 ~2KB)
  • 创建成本极低
  • 由 Go runtime 调度(而不是操作系统)
  • 可以轻松创建成千上万个

🚀 第一个 goroutine

go 复制代码
package main

import (
	"fmt"
	"time"
)

// 定义一个普通函数
func hello() {
	fmt.Println("Hello, world!") // 打印一句话
}

func main() {
	go hello()
	// 使用 go 关键字启动一个 goroutine(协程)
	// 此时 hello() 会在一个新的协程中异步执行
	// main 函数不会等待它执行完

	time.Sleep(time.Second)
	// 让主 goroutine 休眠 1 秒
	// 作用:防止 main 提前退出
	// 如果没有这行代码,程序可能在 hello() 执行前就结束了
}

👉 注意:

👉 1. goroutine 是异步执行的

  • go hello() 不会阻塞
  • main 会继续往下执行

👉 2. main 退出 = 所有 goroutine 结束

  • 这是很多新手最容易踩的坑

goroutine 执行机制

🔥 关键模型:GMP 模型

Go 的调度核心是:

名称 含义
G Goroutine
M 线程(Machine)
P Processor(调度器)

👉 关系:

复制代码
G(任务) → P(队列) → M(执行)

🧠 调度流程(简化版)

  1. goroutine(G)加入队列
  2. P 负责调度 G
  3. M(线程)执行 G
  4. 遇到阻塞 → 切换其他 G

🚀 为什么 goroutine 很轻?

相比传统线程:

对比项 线程 goroutine
创建成本 极低
内存 MB级 KB级
调度 OS Go runtime
切换

goroutine + channel(核心组合)

Go 并发哲学:

不要通过共享内存来通信,而要通过通信来共享内存


📦 channel 基础

go 复制代码
ch := make(chan int)

示例:goroutine 通信

go 复制代码
package main

import "fmt"

// 定义一个 worker 函数,接收一个 int 类型的 channel
func worker(ch chan int) {
	ch <- 100
	// 向 channel 发送数据 100
	// 如果没有接收者,这里会阻塞(很关键)
}

func main() {
	ch := make(chan int)
	// 创建一个无缓冲 channel(同步 channel)
	// 特点:发送和接收必须同时准备好,否则会阻塞

	go worker(ch)
	// 启动 goroutine 执行 worker
	// worker 会尝试向 channel 发送数据

	v := <-ch
	// 从 channel 接收数据
	// 如果没有数据,这里会阻塞,直到有数据写入

	fmt.Println(v)
	// 输出接收到的值:100
}

🔁 带缓冲 channel

go 复制代码
package main

import "fmt"

// 定义 worker 函数,参数是一个 int 类型的 channel
func worker(ch chan int) {
	ch <- 100
	// 向 channel 发送数据 100
	// 因为是带缓冲 channel,所以只要 buffer 没满就不会阻塞
}

func main() {
	ch := make(chan int, 2)
	// 创建一个带缓冲的 channel,容量为 2
	// 表示最多可以暂存 2 个 int

	ch <- 1
	// 第一次发送:放入 buffer[0]

	ch <- 2
	// 第二次发送:放入 buffer[1]
	// 此时 buffer 已满(2/2)

	fmt.Println(<-ch)
	// 从 channel 取出一个值(1)
	// buffer 腾出一个位置

	fmt.Println(<-ch)
	// 再取出一个值(2)
	// 此时 buffer 为空

	go worker(ch)
	// 启动 goroutine 执行 worker
	// 因为 buffer 已经空出空间,所以可以正常写入 100

	fmt.Println(<-ch)
	// 从 channel 取出一个值(100)
	fmt.Println("main function")
	// 主函数继续执行,不会等待 worker
}

输出:

bath 复制代码
1
2
100
main function

实际运行逻辑是:

创建 buffer = 2 的 channel

写入 1、2(buffer 满)

读取 1、2(buffer 清空)

启动 goroutine 写入 100

main 继续执行,读取 100

👉 特点:

  • 不会立即阻塞
  • 类似队列

goroutine 实战场景


🧪 并发任务处理

go 复制代码
package main

import (
	"fmt"
	"time"
)

// 定义一个任务函数,模拟耗时操作
func task(id int) {
	fmt.Println("start", id)
	// 打印任务开始

	time.Sleep(time.Second)
	// 模拟耗时 1 秒的业务逻辑(比如 IO / 网络 / DB)

	fmt.Println("end", id)
	// 打印任务结束
}

func main() {
	for i := 0; i < 10; i++ {
		go task(i)
		// 启动 10 个 goroutine 并发执行 task
		// 每个 goroutine 处理一个 id
	}

	time.Sleep(2 * time.Second)
	// 主 goroutine 休眠 2 秒
	// 作用:防止 main 函数提前退出
	// 否则子 goroutine 还没执行完程序就结束了
}

输出:

bash 复制代码
start 9
start 6
start 4
start 5
start 8
start 0
start 1
start 2
start 7
start 3
end 9
end 6
end 5
end 4
end 0
end 8
end 3
end 1
end 2
end 7

👉 输出是"交错的"

👉 重点:

goroutine 调度是抢占式 + 不可控顺序

  • 10 个 goroutine 同时进入调度队列
  • Go runtime 自动调度执行
  • 执行顺序 完全不确定

🧪 使用 WaitGroup 控制并发

go 复制代码
package main

import (
	"fmt"
	"sync"
)

// 定义一个任务函数,接收 id 和 WaitGroup 指针
func task(id int, wg *sync.WaitGroup) {
	defer wg.Done()
	// defer 保证函数结束时一定调用 Done()
	// 表示该 goroutine 执行完成,计数器 -1

	fmt.Println("task:", id)
	// 模拟任务执行
}

func main() {
	var wg sync.WaitGroup
	// 创建 WaitGroup,用于控制 goroutine 同步

	for i := 0; i < 10; i++ {
		wg.Add(1)
		// 每启动一个 goroutine,计数器 +1
		// 表示"还有一个任务未完成"

		go task(i, &wg)
		// 启动 goroutine 执行任务
		// 注意:传指针,否则会拷贝 wg(错误写法)
	}

	wg.Wait()
	// 阻塞主 goroutine
	// 直到 wg 计数器变为 0(所有任务完成)

}

输出:顺序不一的

bash 复制代码
task: 9
task: 0
task: 1
task: 2
task: 3
task: 4
task: 5
task: 6
task: 7
task: 8

👉 推荐:生产环境必须用 WaitGroup,而不是 sleep

WaitGroup 是 Go 中用于"等待一组 goroutine 完成"的标准同步工具,本质是计数器控制并发生命周期。


🧪 并发安全(Mutex)

go 复制代码
package main

import (
	"fmt"
	"sync"
)

// 全局变量:共享资源(多个 goroutine 会同时访问)
var count int

// 定义互斥锁,用于保护共享变量 count
var mu sync.Mutex

// 定义任务函数,接收 WaitGroup 指针
func add(wg *sync.WaitGroup) {
	defer wg.Done()
	// goroutine 执行完成后通知 WaitGroup -1

	mu.Lock()
	// 加锁:同一时刻只允许一个 goroutine 进入临界区

	count++
	// 临界区:对共享变量进行修改(非原子操作)

	mu.Unlock()
	// 解锁:允许其他 goroutine 进入临界区
}

func main() {
	var wg sync.WaitGroup
	// 用于等待所有 goroutine 执行完成

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		// 每启动一个 goroutine,计数 +1

		go add(&wg)
		// 启动 goroutine 执行加法操作
	}

	wg.Wait()
	// 阻塞主 goroutine,等待所有任务完成

	fmt.Println(count)
	// 输出最终结果:1000
}

👉 Go 设计哲学:

不要通过共享内存通信,而要通过通信共享内存
Mutex 的作用是保证共享资源在并发访问时的"互斥性",从而避免数据竞争,保证程序结果正确。


goroutine 常见坑(必会)


❌ 主 goroutine 提前退出

go 复制代码
go func() {
	fmt.Println("hello")
}()

👉 可能不会执行!

✔ 解决:

  • WaitGroup
  • channel
  • 阻塞 main

❌ 闭包变量问题(经典面试题)

go 复制代码
for i := 0; i < 3; i++ {
	go func() {
		fmt.Println(i)
	}()
}

👉 可能输出:

复制代码
3 3 3

✔ 正确写法:

go 复制代码
for i := 0; i < 3; i++ {
	go func(i int) {
		fmt.Println(i)
	}(i)
}

❌ goroutine 泄漏

go 复制代码
func worker(ch chan int) {
	<-ch // 永远等不到
}

👉 没有关闭 channel → goroutine 永久阻塞

正确写法:

go 复制代码
func worker(ch chan int) {
	for v := range ch {
		fmt.Println(v)
	}
}

主函数:

go 复制代码
ch := make(chan int)

go worker(ch)

ch <- 1
ch <- 2

close(ch) // 👈 关键:关闭 channel

goroutine 调度细节(进阶)


⏱ 抢占式调度(Go 1.14+)

以前:

  • 协程不会主动让出 CPU

现在:

  • Go runtime 会强制抢占

👉 优势:

  • 防止某个 goroutine 长时间占用 CPU

🔄 调度时机

goroutine 切换发生在:

  • channel 阻塞
  • IO 阻塞
  • 系统调用
  • runtime 主动调度

性能优化建议


🚀 控制 goroutine 数量

❌ 错误:

go 复制代码
for {
	go task()
}

✔ 正确(使用 worker pool):

go 复制代码
jobs := make(chan int, 100)

for w := 0; w < 5; w++ {
	go worker(jobs)
}

🚀 使用 sync.Pool 复用对象

减少 GC 压力(高并发场景)


🚀 合理使用 channel

  • 不要滥用
  • 简单场景用锁更高效

总结


🎯 goroutine 核心要点

  • go 关键字开启协程
  • 本质是用户态线程
  • 由 GMP 模型调度
  • 与 channel 配合使用最优雅

🎯 并发三件套

  • goroutine
  • channel
  • sync(WaitGroup / Mutex)

🎯 一句话总结

goroutine 让并发变简单,但并发本身并不简单。


📌 面试高频问题

  1. goroutine 和线程区别?
    答:轻量级线程,由 Go runtime 管理。
  2. GMP 模型是什么?
    答:Go 运行时调度模型,包含 G(goroutine)、M(线程)和 P(处理器)。
  3. channel 是怎么实现的?
    答:基于管道通信,底层实现依赖于 goroutine。
  4. 如何避免 goroutine 泄漏?
    答:确保所有 goroutine 执行完毕,或使用 context 控制。
  5. select 的作用?
    答:多路复用,用于等待多个 channel 操作。

相关推荐
txxzjmzlh2 小时前
Thread 类的基本用法
java·开发语言
machnerrn2 小时前
matlab实现直流伺服电机 PID 控制系统仿真系统(含源码+资料报告+说明文档等)
开发语言·matlab
Hello--_--World2 小时前
JS:this指向、bind、call、apply、知识点与相关面试题
开发语言·javascript·ecmascript
沐知全栈开发2 小时前
CSS Text(文本)
开发语言
lolo大魔王2 小时前
Go语言的文件处理操作
golang
前进吧-程序员2 小时前
现代 C++ 异步编程:从零实现一个高性能 ThreadPool (C++20 深度实践)
开发语言·c++·c++20
Rsun045512 小时前
10、Java 桥接模式从入门到实战
java·开发语言·桥接模式
jieyucx3 小时前
Golang 完整安装与 VSCode 开发环境搭建教程
开发语言·vscode·golang
pearlthriving3 小时前
c++当中的泛型思想以及c++11部分新特性
java·开发语言·c++