一、前言:Go 语言的核心优势 —— 原生并发
Go 被称为21 世纪的 C 语言,两大核心亮点:
- 语法设计简洁、上手门槛低
- 语言层面原生支持并发,内置 GC 自动垃圾回收,无需手动管理内存
Go 并发设计哲学
不同于传统多线程靠锁实现同步,Go 采用通信的方式解决并发竞争:
不要通过共享内存来通信,而要通过通信来共享内存
二、基础概念:并发 vs 并行
1. 并行(Parallelism)
同一时刻,多条指令在不同 CPU 核心同时执行,必须依赖多核硬件。
2. 并发(Concurrency)
同一时刻只有一条指令在 CPU 执行,系统通过时间片轮转快速切换多个任务;宏观看起来多个任务同时运行,微观是交替执行,单核 CPU 即可实现。
举例:单核 CPU 处理 3 个 1s 任务,靠时间片切换实现并发,在有限硬件资源下提升整体处理效率。
核心区分总结
- 并发:同时处理多件事(单核、任务切换)
- 并行:同时做多件事(多核、真正同时执行)
三、Goroutine(协程):Go 并发核心
1. 什么是 Goroutine
Goroutine 是 Go 内置的轻量级协程,可简单理解为轻量任务,是 Go 并行调度的核心:
- 比操作系统线程体积更小、开销极低;
- 底层采用 M:N 调度:成千上万个 goroutine 会复用少量 OS 线程;
- Go 运行时自动管理 goroutine 内存共享,调度成本远低于线程;
- 优势:易用、高效、资源占用极少。
2. 创建 Goroutine
语法:在函数 / 匿名函数前加go关键字,即可新建子协程。
go
// 普通函数开启协程 go newTask() // 匿名函数协程(必须加()执行) go func() { // 协程业务逻辑 }()3. 核心规则:主协程退出,所有子协程直接销毁
main函数本身是主协程,一旦 main 执行完毕,整个进程直接终止,所有未执行完成的子协程会被强制杀死,不会等待。
示例 1:子协程来不及执行
go
package main import "time" func newTask() { for { fmt.Println("子协程运行中") time.Sleep(time.Second) } } func main() { // 开启子协程 go newTask() // 主协程循环2次就退出 i := 0 for { i++ fmt.Println("主协程 i = ", i) time.Sleep(time.Second) if i == 2 { break // main结束,子协程直接销毁,无任何输出 } } }示例 2:死循环阻塞主协程(测试用)
写无限for{}阻塞主协程,防止程序提前退出,方便观察子协程输出:
go
func main() { go newTask() // 死循环阻塞主线程 for {} }四、runtime 包:协程调度控制工具
1. runtime.Gosched ():主动让出 CPU 时间片
作用:当前 goroutine 主动放弃执行权,调度器切换其他就绪协程运行,等其他协程执行完毕后再切回当前协程。
示例:不加 Gosched 时,主协程会优先跑完,子协程无执行机会
go
package main import ( "fmt" "runtime" ) func main() { go func() { for i := 0; i < 5; i++ { fmt.Println("子协程 go") } }() for i := 0; i < 2; i++ { fmt.Println("主协程 hello") runtime.Gosched() // 主动让出时间片,让子协程先执行 } }2. runtime.Goexit ():终止当前协程
作用:立即结束当前所在 goroutine,不会影响其他协程;会优先执行当前协程所有 defer 延迟函数,普通return仅终止当前函数,不会退出协程。
go
package main import ( "fmt" "runtime" ) func test() { defer fmt.Println("执行defer延迟语句") // runtime.Goexit() // 终止整个子协程 return // 仅退出test函数,协程继续往下执行 fmt.Println("该行永远不会打印") } func main() { go func() { fmt.Println("子协程开始") test() fmt.Println("子协程结束") // Goexit()时该行不会执行 }() // 死循环阻塞主协程 for {} }3. runtime.GOMAXPROCS (n):设置并行 CPU 核心上限
- 功能:设置可并行执行 goroutine 的 OS 线程最大数量,返回修改前的旧值;
- 规则:
- n=1:单核运行,无真正并行,仅时间片切换并发;
- n 等于 CPU 物理核数:最大化多核并行能力;
- Go1.5 + 默认值为机器 CPU 核心数;
- 效果:核数越大,多协程交替输出越混乱,并行效率越高。
go
package main import ( "fmt" "runtime" ) func main() { // 设置仅使用1核CPU,返回旧配置 old := runtime.GOMAXPROCS(1) fmt.Println("修改前CPU并行数:", old) for { go fmt.Print(1) fmt.Print(0) } }五、多协程资源竞争问题
场景模拟:多个协程共用同一个打印机(公共资源)
多个 goroutine 同时操作同一共享资源,会出现输出错乱、数据混乱,即并发竞争,需要同步机制解决。
go
package main import "fmt" // 公共打印机资源 func Printer(str string) { for _, data := range str { fmt.Print(string(data)) } fmt.Println() } func person1() { Printer("hello") } func person2() { Printer("world") } func main() { // 两个协程同时抢占打印机,输出错乱 go person1() go person2() // 阻塞主协程观察效果 for {} }解决方案预告
Go 官方推荐使用channel 通道实现 goroutine 同步与通信,解决资源竞争,下一章详细讲解。
六、全文总结(面试 / 复习速记)
- Go 核心优势:语言原生并发,goroutine 轻量高效,GC 自动回收;
- 并发:单核时间片切换;并行:多核同时执行;
- goroutine 创建:
go + 函数,主协程退出则所有子协程销毁; - runtime 三大工具:
Gosched():主动让出 CPU;Goexit():终止当前协程,执行 defer;GOMAXPROCS():控制并行 CPU 核心数;
- 多协程访问公共资源会产生竞争,使用 channel 通信实现同步。