Go语言并发
一,Go并发介绍
Go 语言天生支持并发(Concurrency),这也是它最强大的特性之一。与传统语言通过线程(thread)或进程(process)实现并发不同,Go 引入了轻量级的 goroutine(协程) 和 channel(通道),让并发变得更简单、安全、高效。
并发 vs 并行
并发:逻辑上的同时处理多个任务(即便在单核 CPU 上,也能通过任务切换实现)。
并行:物理上的多个任务在多个 CPU 核上真正同时执行。
Go 实现的是“并发模型”,由 调度器 管理 goroutine 的执行,最终可以在多核 CPU 上实现并行。
Go 并发的三大核心
核心概念
作用
类似物
goroutine
并发的执行单元(轻量线程)
协程 / 线程
channel
goroutine 之间通信的桥梁
管道 / 队列
select
多个 channel 的监听器
IO 多路复用
这些工具让你能优雅地管理多个任务之间的协调,而不用像传统语言那样写大量锁代码。
并发优点(为什么用 Go 并发)
语法简单,使用成本低
调度自动,不用管理线程池
资源开销小,可以开成千上万个 goroutine
内置 channel 避免数据竞争
支持 CSP(通信顺序进程)模型,强调“通信优于共享内存”
二,goroutine 基础
2.1 什么是 goroutine?
goroutine 是 Go 中的并发执行单元,类似于“轻量线程”,但:
占用内存少(初始仅 2KB)
创建开销极小(一个程序可启动数万个)
由 Go 的调度器自动管理(你不需要手动管理线程)
2.2 启动一个 goroutine 的基本语法
go 函数名(参数)
这会在后台启动一个新任务,继续并行运行,而主函数继续往下执行。
示例 1:最简单的 goroutine 程序
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("Hello from goroutine")
}
func main() {
go hello() // 启动 goroutine
fmt.Println("Main function running...")
time.Sleep(1 * time.Second) // 等待一下,确保 goroutine 有机会执行
fmt.Println("Main function ends")
}
输出可能是:
Main function running...
Hello from goroutine
Main function ends
如果你不加 time.Sleep(),程序可能很快退出,后台的 goroutine 就来不及执行。
总结
问题
正解
要点
goroutine 没有完成怎么办?
主线程要等一下
否则程序提前退出
谁调度 goroutine?
Go runtime scheduler
不由主线程控制
主线程先执行?
不一定,是调度结果造成的
goroutine 本质是并发
2.3 goroutine 之间的通信
2.3.1 无缓冲通道
什么是 channel?:channel(通道) 是 Go 中用于 在 goroutine 之间传递数据 的一种类型安全的通信方式。
可以类比为: goroutine A ➜ [channel] ➜ goroutine B,就像传送带,发送端放进去,接收端取出来。
定义通道的语法:
ch := make(chan 类型)
发送和接收:
ch <- 10 // 发送:把 10 放入通道
x := <-ch // 接收:从通道中取出数据并赋值给 x
示例1 :goroutine 用 channel 传数据
package main
import "fmt"
func sendData(ch chan string) {
ch <- "Hello from goroutine" // 发送数据
}
func main() {
ch := make(chan string) // 创建一个字符串通道
go sendData(ch) // 启动一个并发任务
msg := <-ch // 等待接收数据
fmt.Println(msg) // 输出:Hello from goroutine
}
说明:
main() 会阻塞在 <-ch,直到 sendData() 把数据送过来。
channel 默认是 无缓冲的,发送和接收必须同时满足,才会继续执行。
示例 2:双向通信
package main
import "fmt"
func worker(ch chan string) {
fmt.Println("Worker received:", <-ch)
}
func main() {
ch := make(chan string)
go worker(ch)
ch <- "Hello, Channel!"
}
这段代码输出:
Worker received: Hello, Channel!
总结
操作
代码
阻塞行为
发送值到通道
ch <- x
会阻塞直到有人接收
从通道取值
x := <-ch
会阻塞直到有值可读
注意点:无缓冲通道必须要有发送者和接收者,两个都要要不然会卡住报错!fatal error: all goroutines are asleep - deadlock!
2.3.2 缓冲通道(Buffered Channel)
概念:带缓冲的通道可以在 没有接收方的情况下,暂时存储多个值,直到容量满。
示例:
func main() {
ch := make(chan int, 2) // 缓冲区大小为2
ch <- 1 // 不阻塞
ch <- 2 // 不阻塞
// ch <- 3 // 阻塞(如果你取消注释)
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
}
2.3.3 关闭通道
为什么要关闭通道?
通道关闭后,不能再发送数据
接收方可以通过 range 或 ok 判断通道是否关闭,避免死循环
示例:
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
// 再发就 panic
// ch <- 3 // panic: send on closed channel
for val := range ch {
fmt.Println(val)
}
}
判断是否关闭(非 range 写法)
val, ok := <-ch
if !ok {
fmt.Println("通道关闭了")
}
2.3.4 使用 range 遍历通道
用法:当你 range ch,Go 会不断从通道取值,直到通道被关闭。
示例(配合 goroutine):
func producer(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // 发完后关闭通道,告诉接收方“完了”
}
func main() {
ch := make(chan int)
go producer(ch)
for val := range ch {
fmt.Println("收到:", val)
}
}
输出:
收到:0
收到:1
收到:2
收到:3
收到:4
小贴士:什么时候用 close?
通常是发送方关闭通道
接收方判断通道是否关闭,不会报错
接收通道不需要关闭,只有你想用 range 或判断“结束”时才需要
2.4 select 多路复用
select 类似于 switch,但作用于 多个 channel 操作。它会等待其中一个 case 可以执行,然后运行对应代码。
select 语法结构
select {
case <-ch1:
// ch1 有数据可读
case ch2 <- 123:
// 向 ch2 发送数据成功
default:
// 上面都没准备好时执行(非阻塞)
}
示例1 :同时等待两个通道
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "from ch2"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Received", msg1)
case msg2 := <-ch2:
fmt.Println("Received", msg2)
}
}
}
运行结果:
Received from ch1
Received from ch2
问题1:如果两个 case 都准备好了,select 会怎么选择?
其实,Go 运行时在多个准备好的 case 中是随机公平地选择一个,不是默认选第一个,也就是说,当多个 case 同时就绪,select 会随机挑一个执行,避免偏向某个通道。
问题2:default 分支什么时候执行?
default 语句会在所有 case 都不满足(通道阻塞)时立即执行,并且它不会阻塞程序。
问题3:select 是否会阻塞?在哪些情况?
当 有 case 就绪时,select 不阻塞,立即执行其中一个。
当 没有任何 case 就绪且没有 default 时,select 会阻塞,直到某个 case 准备好。
**示例2:带超时的 select **
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
// 模拟异步发送:3秒后发消息
go func() {
time.Sleep(3 * time.Second)
ch <- "hello"
}()
select {
case msg := <-ch:
fmt.Println("收到消息:", msg)
case <-time.After(2 * time.Second):
fmt.Println("接收超时!")
}
}
解析
time.After(2 * time.Second) 返回一个2秒后会收到消息的通道
select 监听两个通道:
ch:等待正常数据
time.After:等待超时信号
先到的那个 case 就被执行
这实现了“2秒内没数据就超时退出”
三,并发控制
3.1 context 包
3.1.1 为什么需要 context?
在你目前掌握的 goroutine 模型中:
goroutine 一旦启动,无法主动被取消
比如:一个 goroutine 在跑死循环,主函数没办法通知它退出
而 context 就是为了解决这种“控制 goroutine 生命周期”的问题。
3.1.2 context 作用总结
功能
示例场景
✅ 控制取消(cancel)
客户端主动关闭连接,后台任务要立刻退出
✅ 设置超时(timeout)
某任务超过 2 秒未完成则中止
✅ 传递请求级别信息
在链路中传递 request-id、用户信息等
3.1.3 基本用法一览
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("被取消了")
return
default:
// 正常处理逻辑
}
}
}(ctx)
// 主函数等待一下,然后取消
time.Sleep(3 * time.Second)
cancel()
3.1.4 context.WithCancel
创建 context 和取消器:
ctx, cancel := context.WithCancel(context.Background())
ctx:传给子 goroutine 使用的 context
cancel():主 goroutine 通过它发送“取消信号”
子 goroutine 接收取消信号:
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("退出")
return
default:
fmt.Println("工作中...")
time.Sleep(500 * time.Millisecond)
}
}
}(ctx)
取消执行:
time.Sleep(2 * time.Second)
cancel() // 通知 goroutine 停止
完整示例代码
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("worker 收到取消信号,退出!")
return
default:
fmt.Println("worker 工作中...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(2 * time.Second)
fmt.Println("主程序取消任务")
cancel()
time.Sleep(1 * time.Second) // 等待 goroutine 打印完
}
3.1.5 context.WithTimeout 自动超时取消机制
场景:我要等一个任务执行完,但我只愿意等 2 秒,超时就放弃。
一句话总结
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
它会在 2 秒后自动取消 context
不需要你手动调用 cancel()(当然可以提前调用)
示例代码:自动超时取消任务
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("worker 被超时取消了,退出!")
return
default:
fmt.Println("worker 正在处理任务...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 通常建议加上,释放资源(即使会自动取消)
go worker(ctx)
// 等待 4 秒看看发生什么
time.Sleep(4 * time.Second)
fmt.Println("主函数结束")
}
3.1.6 context.WithValue():传递请求级别数据
可以把一些和业务逻辑强相关但和函数参数无关的“上下文信息”嵌入 context
常见于 HTTP 请求、链路追踪等场景
示例:
ctx := context.WithValue(context.Background(), "request_id", "abc123")
func doSomething(ctx context.Context) {
if rid, ok := ctx.Value("request_id").(string); ok {
fmt.Println("处理请求 ID:", rid)
}
}
常用于:
request ID
用户身份信息
日志上下文追踪
数据库事务上下文
3.1.7 context propagation:在多个模块、函数间统一传递 ctx
在服务端开发中,ctx 会贯穿从接收请求到发出响应的整个过程。你要:
函数层层传递 ctx context.Context
中间层都可以监听取消、获取元信息
示例:
func handler(ctx context.Context) {
result := doQuery(ctx)
log(ctx, result)
}
3.1.8 自定义 context key 类型(更安全)
type contextKey string
const userIDKey contextKey = "userID"
ctx := context.WithValue(context.Background(), userIDKey, 42)
id := ctx.Value(userIDKey).(int)
避免不同包的 key 互相冲突,属于生产级写法。
3.2 sync 包
sync 包 —— Go 并发中的“结构化协作工具”
3.2.1 sync.WaitGroup
为什么要用 WaitGroup?
你已经知道:启动多个 goroutine 很容易,但怎么知道它们都什么时候结束呢?
用 WaitGroup,你可以:
启动多个任务
主线程阻塞等待,直到它们全部完成
不再用 sleep 这种不靠谱的方式等待任务结束
sync.WaitGroup 的 3 个核心方法
方法
含义
wg.Add(n)
表示将有 n 个任务需要等待
wg.Done()
每个任务执行完时调用,表示减少一个待完成任务
wg.Wait()
阻塞等待直到所有任务完成
示例代码
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 通知 WaitGroup:这个任务完成了!
fmt.Printf("Worker %d 开始工作...\n", id)
time.Sleep(1 * time.Second)
fmt.Printf("Worker %d 完成工作!\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 每启动一个任务,Add(1)
go worker(i, &wg)
}
wg.Wait() // 阻塞等待,直到所有任务都 Done()
fmt.Println("所有任务完成,主程序退出")
}
解释步骤:
wg.Add(1)
表示“我要等待一个 goroutine”
go worker(i, &wg)
启动 goroutine,并传入 wg 指针
defer wg.Done()
任务执行完,告诉 wg 减去一个待完成项(和 Add 对应)
wg.Wait()
阻塞主线程,直到所有 Done() 都调用完成
3.2.2 sync.Mutex
为什么需要 sync.Mutex?
当多个 goroutine 同时修改同一个变量时,就会发生竞态条件(race condition),导致结果不可预测。
例如:多个任务同时对 count++,可能会导致丢数据或重复计数。
sync.Mutex 基本用法
var mu sync.Mutex
mu.Lock() // 加锁
// 访问共享资源
mu.Unlock() // 解锁
Lock():阻塞直到获取锁
Unlock():释放锁
多个 goroutine 必须在访问共享变量时加锁+解锁
示例:多个 goroutine 对同一个变量加 1,如果不加锁会出错
不加锁(会发生竞态)
package main
import (
"fmt"
"sync"
)
func main() {
var count int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
count++ // 多个 goroutine 同时修改 -> 错误
wg.Done()
}()
}
wg.Wait()
fmt.Println("最终结果(错误):", count)
}
输出可能是 900、多次跑不一样
可以用 go run -race main.go 检测数据竞争
正确版:使用 sync.Mutex
package main
import (
"fmt"
"sync"
)
func main() {
var count int
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
mu.Lock()
count++ // 现在只有一个 goroutine 可以执行这里
mu.Unlock()
wg.Done()
}()
}
wg.Wait()
fmt.Println("最终结果(正确):", count)
}
每次输出都是 1000,且不会有数据竞争
3.2.3 sync.Once
sync.Once:只执行一次的并发原语
sync.Once 是 Go 并发包中最小但非常实用的工具之一,用来确保某段代码 在整个程序生命周期中只执行一次,即使有多个 goroutine 并发调用它也不例外。
用法场景:
初始化数据库连接
只加载一次配置文件
启动日志服务
全局对象懒加载
使用规则非常简单
var once sync.Once
once.Do(func() {
// 只会执行一次
})
无论你 once.Do() 被调用多少次,传入的函数只会执行一次。
示例:多个 goroutine 并发调用,只初始化一次
package main
import (
"fmt"
"sync"
)
var once sync.Once
func initService() {
fmt.Println("🔧 初始化服务")
}
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d 开始...\n", id)
once.Do(initService)
fmt.Printf("Worker %d 工作完成\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
}
输出示例(不管几次调用,只打印一次初始化):
Worker 1 开始...
🔧 初始化服务
Worker 1 工作完成
Worker 3 开始...
Worker 2 开始...
Worker 4 开始...
Worker 5 开始...
Worker X 工作完成...
(由于并发,顺序可能不一样,但“初始化服务”绝对只出现一次)
注意事项:
once.Do() 只能确保函数只执行一次,即使它 panics,也算“执行过”了
如果你想“只在第一次成功时算数”,需要你手动加判断和锁
once 变量不能重置,它只适合“生命周期内一次性初始化”的操作
实战建议:
用于懒加载场景比全局 init() 更可控
不建议用于依赖上下文的初始化(如有时效、配置变更等)
3.2.4 sync.Cond
它是干嘛用的?
sync.Cond 是 条件变量,用来实现更复杂的协作,比如:
一个或多个 goroutine 必须等待某个条件成立时才能继续执行
它解决的是这个问题:
多个 goroutine 在等待「某个条件变为 true」时,要等主线程或其他 goroutine 发通知才继续做事。
比如:
有人生产了数据,才能消费(典型的“生产者-消费者模型”)
有资源空出来,才能继续申请(比如限流器)
结构 & 方法
cond := sync.NewCond(&sync.Mutex{}) // 创建条件变量
cond.L.Lock() // 上锁
// 条件不满足
cond.Wait() // 等待信号(自动解锁并挂起)
cond.L.Unlock() // 解锁(放到 finally/defer 里)
cond.Signal() // 发出一个通知,唤醒一个 Wait()
cond.Broadcast() // 发出广播,唤醒所有 Wait()
示例:控制消费者等待产品生产完再处理
package main
import (
"fmt"
"sync"
"time"
)
var dataReady = false
var cond = sync.NewCond(&sync.Mutex{})
func consumer(id int, wg *sync.WaitGroup) {
defer wg.Done()
cond.L.Lock()
for !dataReady { // 条件不满足就等待
fmt.Printf("消费者 %d 等待数据...\n", id)
cond.Wait()
}
fmt.Printf("消费者 %d 收到数据,开始处理!\n", id)
cond.L.Unlock()
}
func producer() {
time.Sleep(2 * time.Second) // 模拟生产耗时
cond.L.Lock()
fmt.Println("👷♂️ 生产者准备好了数据,发出通知!")
dataReady = true
cond.Broadcast() // 唤醒所有等待的消费者
cond.L.Unlock()
}
func main() {
var wg sync.WaitGroup
// 启动多个消费者
for i := 1; i <= 3; i++ {
wg.Add(1)
go consumer(i, &wg)
}
// 启动生产者
go producer()
wg.Wait()
fmt.Println("所有消费者处理完毕")
}
输出示例:
消费者 1 等待数据...
消费者 2 等待数据...
消费者 3 等待数据...
👷♂️ 生产者准备好了数据,发出通知!
消费者 1 收到数据,开始处理!
消费者 2 收到数据,开始处理!
消费者 3 收到数据,开始处理!
所有消费者处理完毕
总结 sync.Cond 的适用场景
场景
用不用 Cond?
固定数量任务,等它们做完(用 WaitGroup)
❌
多个任务等待一个条件成立时才执行
✅ 非常适合
实现生产者-消费者模型
✅ 很适合
想唤醒一组被挂起的 goroutine
✅ 用 Broadcast
注意事项
Wait() 必须在 Lock() 保护下调用,否则会 panic
Wait() 自动释放锁,等被唤醒后会重新自动加锁
如果多个 goroutine 在 Wait(),可以用 Broadcast() 一次唤醒全部
3.2.5 sync.RWMutex
**什么是 sync.RWMutex**
RWMutex 是读写锁,支持:
多个 goroutine 并发读
但只能一个 goroutine 写,且写时禁止其他读或写
用法对比
操作
说明
RLock()
获取读锁(多个读可同时持有)
RUnlock()
释放读锁
Lock()
获取写锁(独占)
Unlock()
释放写锁
应用场景
场景
适合用 RWMutex?
配置缓存,多个协程并发读取偶尔更新
✅ 非常适合
并发写入数据结构
❌ 用普通 Mutex
数据初始化后只读,不再写
✅ 用 RLock 替代 Lock,性能更优
示例代码:一个并发安全的字典
package main
import (
"fmt"
"sync"
"time"
)
type SafeMap struct {
mu sync.RWMutex
data map[string]string
}
func NewSafeMap() *SafeMap {
return &SafeMap{
data: make(map[string]string),
}
}
func (s *SafeMap) Get(key string) string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.data[key]
}
func (s *SafeMap) Set(key, value string) {
s.mu.Lock()
defer s.mu.Unlock()
s.data[key] = value
}
func main() {
sm := NewSafeMap()
// 写操作
go func() {
for i := 0; i < 5; i++ {
sm.Set("foo", fmt.Sprintf("val-%d", i))
time.Sleep(300 * time.Millisecond)
}
}()
// 多个读操作
for i := 0; i < 3; i++ {
go func(id int) {
for j := 0; j < 5; j++ {
val := sm.Get("foo")
fmt.Printf("Goroutine %d 读到 foo = %s\n", id, val)
time.Sleep(200 * time.Millisecond)
}
}(i)
}
time.Sleep(3 * time.Second)
}
输出示例(可能顺序不同):
Goroutine 0 读到 foo = val-0
Goroutine 2 读到 foo = val-0
Goroutine 1 读到 foo = val-1
...
3.2.6 sync.Map
Go 中普通的 map 不是并发安全的:多个 goroutine 读写同一个 map 会导致程序崩溃(fatal error: concurrent map read and map write)。
而 sync.Map 是 Go 标准库中提供的内置并发安全 map,不需要我们手动加锁!
典型应用场景
高并发读多写少的场景
缓存:如用户 Session、连接池、配置项等
用于避免加锁逻辑复杂的手动 map 管理
和普通 map 的区别?
特点
普通 map
sync.Map
并发安全性
❌ 非并发安全
✅ 并发安全
泛型支持(Go1.18+)
✅ 支持泛型
❌ 不能用泛型
语法
map[key] = val
.Store(key, val)
常用方法
方法
说明
Store(key, value)
设置键值(写入)
Load(key)
获取值(读取)
LoadOrStore(key, value)
读取或写入(如果不存在就写入)
Delete(key)
删除键
Range(func(key, value) bool)
遍历所有 key/value,返回 false 停止
示例代码:使用 sync.Map 做并发安全缓存
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var m sync.Map
// 写入
m.Store("name", "Go语言")
m.Store("version", "1.22")
// 读取
if val, ok := m.Load("name"); ok {
fmt.Println("name =", val)
}
// LoadOrStore
val, loaded := m.LoadOrStore("author", "Rob Pike")
if loaded {
fmt.Println("author 已存在,值为:", val)
} else {
fmt.Println("author 是新设置的:", val)
}
// 删除
m.Delete("version")
// 遍历
m.Range(func(k, v any) bool {
fmt.Printf("Key: %v, Value: %v\n", k, v)
return true
})
// 并发写入测试
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
key := fmt.Sprintf("key-%d", i)
m.Store(key, i)
}(i)
}
wg.Wait()
// 验证写入结果
fmt.Println("并发写入完成,当前 map 内容:")
m.Range(func(k, v any) bool {
fmt.Printf("%v: %v\n", k, v)
return true
})
}
输出示例:
name = Go语言
author 是新设置的:Rob Pike
Key: name, Value: Go语言
Key: author, Value: Rob Pike
并发写入完成,当前 map 内容:
key-1: 1
key-7: 7
...
注意事项
限制
描述
不能用泛型
所有 key、value 类型都是 any(interface{})
不适合频繁写
大量写操作可能效率低(有分片机制)
不支持直接取值
不能 m["key"],只能 Load/Store
总结什么时候用 sync.Map
用场景
是否推荐
高并发读、低频写
✅ 推荐
map 结构很大,遍历频繁
✅ 推荐
对类型要求严格、结构复杂
❌ 不推荐,用普通 map + RWMutex 更灵活
3.2.7 sync.Pool
什么是 sync.Pool?
sync.Pool 提供了一种缓存临时对象的机制,当你需要频繁创建和销毁对象时,可以把对象放到池里重复利用,避免重复分配内存。
适用场景
高并发服务中重复使用的临时对象,比如缓冲区(byte slice)、结构体等
减少 GC 负担,提升性能
典型应用:网络包处理、JSON 编解码缓存等
主要接口和用法
var pool = sync.Pool{
New: func() any {
return new(SomeType) // 对象创建函数
},
}
obj := pool.Get().(*SomeType) // 从池里取对象,没有时调用 New
// 使用 obj...
pool.Put(obj) // 用完后放回池中
示例代码
package main
import (
"fmt"
"sync"
)
type Buffer struct {
data []byte
}
func main() {
pool := sync.Pool{
New: func() any {
fmt.Println("创建新对象")
return &Buffer{
data: make([]byte, 1024),
}
},
}
// 从池中获取对象
buf := pool.Get().(*Buffer)
fmt.Println("拿到对象,长度:", len(buf.data))
// 使用后放回池中
pool.Put(buf)
// 再次获取,复用上次的对象,不会再创建
buf2 := pool.Get().(*Buffer)
fmt.Println("再次拿到对象,长度:", len(buf2.data))
}
输出示例
创建新对象
拿到对象,长度: 1024
再次拿到对象,长度: 1024
注意事项
池里的对象可能随时被回收(GC),不保证一定存在
适合临时对象,不适合长期持有
不需要显式清空对象(池的对象可能是“脏”的,取出后要手动初始化)
不保证线程安全之外的操作(比如你放入后改对象,还是要自行同步)
总结
特点
说明
减少分配和 GC
复用临时对象,减少内存分配
不保证对象持续存在
对象可能被 GC 回收
适合高频创建场景
例如缓冲区、临时数据
3.3 time 包
主要类型和函数
类型/函数
作用
time.Sleep(d)
阻塞当前 goroutine 指定时间
time.After(d)
返回一个通道,d 时间后会收到当前时间
time.NewTimer(d)
创建一个定时器,d 时间后触发一次
time.NewTicker(d)
创建一个滴答器,每隔 d 时间触发一次
time.Now()
返回当前时间
典型场景示例
time.After 实现超时控制
select {
case msg := <-ch:
fmt.Println("收到消息:", msg)
case <-time.After(2 * time.Second):
fmt.Println("等待超时")
}
使用 time.Timer 实现超时(可停止定时器)
timer := time.NewTimer(2 * time.Second)
go func() {
<-timer.C
fmt.Println("超时了")
}()
// 这里可以根据需要调用 timer.Stop() 来取消定时器
使用 time.Ticker 实现周期任务
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for i := 0; i < 5; i++ {
<-ticker.C
fmt.Println("每秒执行一次任务")
}
time.Sleep 阻塞当前 goroutine
fmt.Println("开始睡眠")
time.Sleep(2 * time.Second)
fmt.Println("睡眠结束")
小总结
超时控制最常用的是 time.After 和 time.Timer
周期任务用 time.Ticker
Sleep 则是简单暂停 goroutine
3.4 runtime 包
runtime 包作用
控制和查询 Go 程序运行时行为
管理 goroutine 调度和系统资源
调试和性能分析辅助
常用功能和函数
设置 CPU 使用核数
runtime.GOMAXPROCS(n int) int
设置 Go 程序可同时使用的最大 CPU 核数
返回之前的设置值
默认是 CPU 核数
示例:
fmt.Println("默认 CPU 核数:", runtime.GOMAXPROCS(0))
runtime.GOMAXPROCS(4) // 限制使用4核
获取当前 goroutine 数量
runtime.NumGoroutine() int
返回当前运行的 goroutine 数
方便调试和性能监控
示例:
fmt.Println("当前 goroutine 数量:", runtime.NumGoroutine())
触发垃圾回收
runtime.GC()
主动触发一次垃圾回收,通常不推荐频繁调用
用于调试或特殊场景的内存管理
获取调用栈信息
runtime.Caller(skip int) (pc uintptr, file string, line int, ok bool)
获取当前调用栈某一层的信息
skip 表示跳过的调用层数,0 表示当前函数
堆栈打印(调试用)
runtime.Stack(buf []byte, all bool) int
把当前所有 goroutine 的堆栈信息写入 buf,返回写入长度
典型使用场景
性能调优:调整 GOMAXPROCS 控制 CPU 利用
监控:统计活跃 goroutine 数,检测 goroutine 泄漏
调试:查看堆栈,获取调用信息
简单示例
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
fmt.Println("默认 CPU 核数:", runtime.GOMAXPROCS(0))
runtime.GOMAXPROCS(2)
fmt.Println("设置 CPU 核数为 2")
for i := 0; i < 5; i++ {
go func(id int) {
fmt.Printf("Goroutine %d 启动\n", id)
time.Sleep(2 * time.Second)
}(i)
}
time.Sleep(500 * time.Millisecond)
fmt.Println("当前 goroutine 数:", runtime.NumGoroutine())
}
3.5 sync/atomic包
sync/atomic 是 Go 中用于原子操作的包,专门提供无锁的基础类型读写和修改操作,非常适合在高并发环境下对简单数据进行高效、线程安全的操作。
为什么用 sync/atomic?
传统的 sync.Mutex 需要加锁、解锁,有一定开销
原子操作直接调用 CPU 指令,效率更高,适合简单数字计数等场景
避免了加锁带来的阻塞,提升性能
常用函数
函数
功能
atomic.AddInt32(addr *int32, delta int32)
对 int32 变量执行原子加操作
atomic.LoadInt32(addr *int32)
原子读取 int32 变量
atomic.StoreInt32(addr *int32, val int32)
原子写入 int32 变量
atomic.CompareAndSwapInt32(addr *int32, old, new int32)
比较并交换 int32 变量
同理也有 Int64、Uint32、Uint64、Pointer 等版本。
示例代码:计数器
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var count int64 = 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&count, 1) // 原子加1
}()
}
wg.Wait()
fmt.Println("最终计数:", count)
}
使用建议
适合简单数值计数、状态标志等场景
不适合复杂数据结构,需要用锁保护
使用时地址必须是变量地址,不能是临时变量或常量
CompareAndSwap 用于实现无锁同步算法(CAS)