- 后端
- 示例工程
【免费下载链接】go-gin-example
An example of gin
本篇技术指南以 vendor/github.com/modern-go/concurrent/README.md 为骨架,系统讲解 modern-go/concurrent 库的两大核心能力:兼容旧版本 Go 的线程安全concurrent.Map,以及可统一取消、自动兜底 panic 的 goroutine 执行器concurrent.Executor。本文同时结合该库在仓库内的全部源码(executor.go、unbounded_executor.go、go_above_19.go、go_below_19.go),还原其底层实现原理,并给出在 Gin 项目中可落地的使用方案。读完本文,你将掌握:如何用concurrent.Map写出跨 Go 版本可移植的并发安全缓存,以及如何用UnboundedExecutor管理后台 goroutine 的生命周期、优雅退出与 panic 恢复。
一、背景:为什么 go-gin-example 的 vendor 里会有 concurrent
go-gin-example 是一个基于 Gin 的示例项目,其依赖关系记录在 go.mod 与 go.sum 中。在go.sum里可以看到github.com/modern-go/concurrent存在两个版本的哈希记录(v0.0.0-20180228061459-e0a39a4cb421与v0.0.0-20180306012644-bacd9c7ef1dd),在 go.mod 的require块中它与github.com/json-iterator/go v1.1.7 // indirect等并列出现,且被标记为// indirect。
从依赖关系看,本项目自身源码(routers/、service/、models/、pkg/等目录)并没有直接 import 该库——搜索非 vendor 目录下的 Go 文件也找不到modern-go/concurrent的引用——它是作为传递性间接依赖被引入并随项目一起 vendored 的(典型链路是 Gin 框架依赖的 JSON 序列化库 json-iterator 内部使用它)。这意味着:concurrent 是 go-gin-example 依赖树中真实存在、可被复用的并发基础设施,理解它的用法,既有助于排查依赖行为,也可以直接在自己的 Gin 业务代码中 import 使用。
二、concurrent.Map:跨版本可移植的并发安全 Map
2.1 设计动机
官方sync.Map在 Go 1.9 才引入。为了在不牺牲并发安全的前提下让代码在 Go 1.9 之前的老版本上也能编译运行,concurrent 库提供了concurrent.Map这一层"封装"。README 的原话是:
because sync.Map is only available in go 1.9, we can use concurrent.Map to make code portable
2.2 两种实现与构建标签
concurrent.Map 通过 Go 构建标签(build tag)实现"一套 API、两套底层实现":
- go_above_19.go 文件首行声明
//+build go1.9,当 Go 版本 >= 1.9 时,Map只是对标准库sync.Map的薄封装:
//+build go1.9 package concurrent import "sync" // Map is a wrapper for sync.Map introduced in go1.9 type Map struct { sync.Map } // NewMap creates a thread safe Map func NewMap() *Map { return &Map{} }由于内嵌了sync.Map,Load、Store、Delete、Range等标准方法全部直接继承。
- go_below_19.go 文件首行声明
//+build !go1.9,当 Go 版本 < 1.9 时,Map退化为"sync.RWMutex+ 普通 map"的实现:
//+build !go1.9 package concurrent import "sync" // Map implements a thread safe map for go version below 1.9 using mutex type Map struct { lock sync.RWMutex data map[interface{}]interface{} } // NewMap creates a thread safe map func NewMap() *Map { return &Map{ data: make(map[interface{}]interface{}, 32), } } // Load is same as sync.Map Load func (m *Map) Load(key interface{}) (elem interface{}, found bool) { m.lock.RLock() elem, found = m.data[key] m.lock.RUnlock() return } // Store is same as sync.Map Store func (m *Map) Store(key interface{}, elem interface{}) { m.lock.Lock() m.data[key] = elem m.lock.Unlock() }可以看到:读操作加RLock(读锁可并发),写操作加Lock(写锁互斥),初次分配时预置了容量 32 的底层 map 以减少扩容次数。无论编译进哪一套实现,对外 API 完全一致,这正是"portable"的体现。
2.3 使用示例(README 原文)
m := concurrent.NewMap() m.Store("hello", "world") elem, found := m.Load("hello") // elem will be "world" // found will be true2.4 在 Gin 项目中的典型场景
在 Web 服务里,concurrent.Map非常适合作为进程内的轻量级共享状态容器,例如:
// 模拟:API 层用 concurrent.Map 缓存"用户在线标记" var onlineUsers = concurrent.NewMap() // 登录成功时写入 onlineUsers.Store(userID, true) // 校验时读取 if v, ok := onlineUsers.Load(userID); ok && v.(bool) { // 在线 }需要提醒的是:Load返回的elem是interface{},使用时需要类型断言;高并发读多写少的场景可优先考虑这种基于 RWMutex/sync.Map的容器,但它不适用于需要"按 key 做原子复合操作"(如 read-modify-write)的场景,那类需求更适合sync.Map.LoadOrStore或显式加锁。
三、concurrent.Executor:带所有权与可取消能力的 goroutine 执行器
3.1 核心理念
README 用一句话概括 Executor 的价值:
attach goroutine to executor instance, so that we can cancel it by stop the executor with Stop/StopAndWait/StopAndWaitForever, and handle panic by callback: the default behavior will no longer crash your application
即:把 goroutine "挂靠"到执行器实例上,让执行器对 goroutine 拥有明确的所有权,从而获得两个能力:
- 通过停止执行器来统一取消其名下所有 goroutine;
- 通过回调统一处理 panic,默认行为下 goroutine 崩溃不再拖垮整个应用进程。
接口定义位于 executor.go:
// Executor replace go keyword to start a new goroutine // the goroutine should cancel itself if the context passed in has been cancelled // the goroutine started by the executor, is owned by the executor // we can cancel all executors owned by the executor just by stop the executor itself // however Executor interface does not Stop method, the one starting and owning executor // should use the concrete type of executor, instead of this interface. type Executor interface { // Go starts a new goroutine controlled by the context Go(handler func(ctx context.Context)) }注意接口注释中的两个设计要点:
- 执行器传入的
ctx是协作式取消的信号,goroutine 内部的select必须主动监听ctx.Done()并自行退出,执行器不会强制杀线程; Executor接口本身没有 Stop 方法,因此调用方应持有具体类型(如*UnboundedExecutor)来执行停止操作。
3.2 UnboundedExecutor:不限制存活 goroutine 数量的执行器
unbounded_executor.go 中实现的UnboundedExecutor是 README 示例使用的具体类型,其结构如下:
type UnboundedExecutor struct { ctx context.Context cancel context.CancelFunc activeGoroutinesMutex *sync.Mutex activeGoroutines map[string]int HandlePanic func(recovered interface{}, funcName string) }ctx/cancel:由context.WithCancel(context.TODO())创建,cancel是统一取消的开关;activeGoroutines:以"文件:行号"为 key 的计数 map,配合互斥锁跟踪每个 goroutine 的存活状态;HandlePanic:可覆盖的 panic 回调(nil 时走包级默认HandlePanic)。
工厂函数NewUnboundedExecutor()创建实例;同时包级预置了一个生命周期与整个程序一致的全局单例:
// GlobalUnboundedExecutor has the life cycle of the program itself var GlobalUnboundedExecutor = NewUnboundedExecutor()注释特别强调:GlobalUnboundedExecutor期望主函数(main)在退出前主动调用 stop,它并不会"神奇地感知" main 的退出。
3.3 Go 方法:启动 + 追踪 + panic 兜底
Go方法(unbounded_executor.go)在go关键字之上追加了三层逻辑:
func (executor *UnboundedExecutor) Go(handler func(ctx context.Context)) { pc := reflect.ValueOf(handler).Pointer() f := runtime.FuncForPC(pc) funcName := f.Name() file, line := f.FileLine(pc) executor.activeGoroutinesMutex.Lock() defer executor.activeGoroutinesMutex.Unlock() startFrom := fmt.Sprintf("%s:%d", file, line) executor.activeGoroutines[startFrom] += 1 go func() { defer func() { recovered := recover() // if you want to quit a goroutine without trigger HandlePanic // use runtime.Goexit() to quit if recovered != nil { if executor.HandlePanic == nil { HandlePanic(recovered, funcName) } else { executor.HandlePanic(recovered, funcName) } } executor.activeGoroutinesMutex.Lock() executor.activeGoroutines[startFrom] -= 1 executor.activeGoroutinesMutex.Unlock() }() handler(executor.ctx) }() }关键机制:
- 启动前登记:通过
reflect拿到 handler 的函数指针,再用runtime.FuncForPC/FileLine定位其源码位置,拼成"file:line"作为唯一标识并计数 +1; - panic 自动恢复:
defer中调用recover(),若捕获到 panic,则调用HandlePanic回调(实例级优先,nil 时回落到包级默认HandlePanic),应用进程不会因此崩溃; - 退出时注销:无论正常返回还是 panic,都会把对应计数 -1,为后续"等待所有 goroutine 退出"提供依据。
源码注释还给出一个补充约定:如果希望 goroutine 静默退出、不触发HandlePanic,应使用runtime.Goexit()而不是 panic。
包级默认的 panic 处理器定义在同一文件顶部:
// HandlePanic logs goroutine panic by default var HandlePanic = func(recovered interface{}, funcName string) { ErrorLogger.Println(fmt.Sprintf("%s panic: %v", funcName, recovered)) ErrorLogger.Println(string(debug.Stack())) }默认行为是:打印出 panic 来源的函数名、panic 值以及完整调用栈(debug.Stack())。
3.4 停止机制:Stop / StopAndWait / StopAndWaitForever
UnboundedExecutor提供三种停止方式(unbounded_executor.go):
// Stop cancel all goroutines started by this executor without wait func (executor *UnboundedExecutor) Stop() { executor.cancel() } // StopAndWaitForever cancel all goroutines started by this executor and // wait until all goroutines exited func (executor *UnboundedExecutor) StopAndWaitForever() { executor.StopAndWait(context.Background()) } // StopAndWait cancel all goroutines started by this executor and wait. // Wait can be cancelled by the context passed in. func (executor *UnboundedExecutor) StopAndWait(ctx context.Context) { executor.cancel() for { oneHundredMilliseconds := time.NewTimer(time.Millisecond * 100) select { case <-oneHundredMilliseconds.C: if executor.checkNoActiveGoroutines() { return } case <-ctx.Done(): return } } }三者区别清晰:
Stop():只调用cancel()广播取消信号,不等待goroutine 退出,立即返回;StopAndWaitForever():取消后无限期等待,直到checkNoActiveGoroutines()确认所有 goroutine 计数归零才返回;StopAndWait(ctx):取消后轮询等待(每 100ms 检查一次存活计数),但可用传入的 ctx 打断等待,避免"永远等不到退出"的死等。
配套的checkNoActiveGoroutines在仍有存活 goroutine 时会通过InfoLogger打印提示(包含startFrom定位与count计数),方便排查"为什么还没退出"。
3.5 README 完整示例:定时任务 + 优雅停止
README 给出的完整示例(其中everyMillisecond应为everyMillisecond,此处保留原意)如下:
executor := concurrent.NewUnboundedExecutor() executor.Go(func(ctx context.Context) { everyMillisecond := time.NewTicker(time.Millisecond) for { select { case <-ctx.Done(): fmt.Println("goroutine exited") return case <-everyMillisecond.C: // do something } } }) time.Sleep(time.Second) executor.StopAndWaitForever() fmt.Println("executor stopped")运行流程:
NewUnboundedExecutor()创建执行器;Go(...)启动一个每毫秒触发一次 ticker 的后台循环,循环体内通过select同时监听ctx.Done()与 ticker 信号;- 主 goroutine
Sleep一秒后调用StopAndWaitForever(),执行器广播取消并阻塞等待; - 后台循环收到
ctx.Done()后打印 "goroutine exited" 并return,计数归零,StopAndWaitForever返回,最后打印 "executor stopped"。
这套模式正是 Go 服务优雅关机的标准骨架:停止信号 -> 取消 context -> 协程协作退出 -> 等待全部退出 -> 主程序退出。
四、日志基础设施:ErrorLogger 与 InfoLogger
log.go 定义了执行器的两个日志出口:
var ErrorLogger = log.New(os.Stderr, "", 0) var InfoLogger = log.New(ioutil.Discard, "", 0)ErrorLogger:默认输出到os.Stderr,panic 信息(函数名、panic 值、调用栈)都会走这里,可替换为自定义io.Writer以对接日志系统;InfoLogger:默认输出到ioutil.Discard(即默认关闭),checkNoActiveGoroutines等待期间的提示信息走这里,需要排查时可将它切换到真实 writer 开启诊断。
五、在 Gin 服务中的集成实战建议
结合 go-gin-example 这样的 Web 项目,concurrent 库可落地的组合方案如下:
场景 A:进程内共享缓存/状态用concurrent.Map替代裸 map + 手写锁,例如在 pkg/gredis 这类缓存模块中维护进程级标记位(注意该库主要用于跨版本兼容,Go >= 1.9 的环境直接使用sync.Map亦可,二者 API 一致)。
场景 B:后台任务的生命周期管理把"启动时持续运行、退出前必须收尾"的 goroutine(如心跳上报、指标采集、日志刷盘)统一交给UnboundedExecutor:
executor := concurrent.NewUnboundedExecutor() executor.Go(func(ctx context.Context) { for { select { case <-ctx.Done(): // 刷盘/清理 return default: // 周期任务 } } }) // ... 服务退出流程 executor.StopAndWaitForever() // 优雅停机,确保后台任务收尾场景 C:全局兜底防崩溃借助GlobalUnboundedExecutor或自定义HandlePanic回调(对接 pkg/logging 的日志组件),让任何意外 panic 的后台 goroutine 都被记录而非直接击穿整个进程。
六、小结
| 能力 | 对应实现 | 源码位置 |
|---|---|---|
| 跨版本线程安全 Map | concurrent.NewMap() | go_above_19.go / go_below_19.go |
| goroutine 执行器接口 | Executor.Go(ctx) | executor.go |
| 无上限执行器 + 优雅停止 | UnboundedExecutor(Stop/StopAndWait/StopAndWaitForever) | unbounded_executor.go |
| 全局单例执行器 | GlobalUnboundedExecutor | unbounded_executor.go |
| panic 兜底与日志 | HandlePanic/ErrorLogger/InfoLogger | unbounded_executor.go / log.go |
一句话总结:concurrent.Map解决的是"并发安全的共享数据",concurrent.Executor解决的是"受控的 goroutine 生命周期",二者结合可以让 Web 服务中的并发代码既安全又可优雅停机。在 go-gin-example 中它作为间接依赖随项目 vendored(记录于 go.sum),当你需要这种能力时,直接import "github.com/modern-go/concurrent"即可复用,无需额外引入新依赖。
- 后端
- 示例工程
【免费下载链接】go-gin-example
An example of gin
相关推荐
Go 并发工具箱 modern-go/concurrent 深入解析:线程安全 Map 与可取消 Executor
Go 并发工具箱 modern go/concurrent 深入解析:线程安全 Map 与可取消 Executor modern go/concurrent 是
测试云原生质量保障Sliver 项目中的 Go 并发利器:modern-go/concurrent 的 Map 与 Executor 实践解析
Sliver 项目中的 Go 并发利器:modern go/concurrent 的 Map 与 Executor 实践解析 导读 modern go/conc
网络安全KubeSphere 依赖解析:modern-go/concurrent 的并发 Map 与可取消 Goroutine Executor 实战指南
KubeSphere 依赖解析:modern go/concurrent 的并发 Map 与可取消 Goroutine Executor 实战指南 导读 git
后端云原生容器编排微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考