v1.2.0
v1.2.0 — 破坏性版本:4 轮评审收口 + 主线A 并发治理统一
发布日期:2026-07-04
版本:v1.2.0(破坏性版本)
上一版本:v1.1.1
验证:go vet+go build+go test -race ./...全绿
概述
v1.2.0 是 xlgo 框架的破坏性版本,核心是 4 轮对抗性评审收口的全部 CRITICAL/HIGH/MEDIUM 修复,加上 主线A「包级可变全局并发治理统一」——把框架内所有包级可变全局一律收敛到 atomic.Pointer / sync.Once / 锁,根除"局部全对、全局全错"的并发安全隐患。
本轮无新增功能,全部为结构性修复与对齐。三条主线闭合:
- 包级可变全局保护统一(config/cache/jwt/trace/database/redis/storage/validation/router/response 全部
atomic.Pointer) - 资源生命周期在重建路径上彻底释放(trace sync.Once、app OnReady、http transport、ws Hub)
- 跨文件失败语义/契约一致(Redis 不可用、健康检查、WithConfig 契约)
⚠️ 破坏性变更(升级前必读)
1. 包级可变全局一律 atomic.Pointer(主线A)
database.DefaultRedis / database.DefaultManager / storage.DefaultStorage / validation.Validator 由裸指针改为 atomic.Pointer[T],消除无锁置换/读取的数据竞争。
迁移:
| 旧 API | 新 API |
|---|---|
database.DefaultRedis.Init(cfg) |
database.InitRedis(cfg) 或 database.DefaultRedis.Load().Init(cfg) |
database.DefaultManager = myDB |
database.SetDefaultManager(myDB) |
database.DefaultManager.Master() |
database.GetDB() 或 database.DefaultManager.Load().Master() |
storage.DefaultStorage.X(...) |
storage.X(...) facade 或 storage.DefaultStorage.Load().X(...) |
validation.Validator.Struct(s) |
validation.ValidateStruct(s) 或 validation.Validator.Load().Struct(s) |
2. jwt.DefaultJWT 包级变量删除
→ jwt.GetDefaultJWT() / jwt.SetDefaultJWTManager()
3. repository.FindWhereOrdered / FindPageWhereOrdered 签名变更(H-15)
args []any → args ...any;因 Go 变长参数须为末尾参数,order 前置于 query。
// 新签名
FindWhereOrdered(ctx, order, query string, args ...any)
FindPageWhereOrdered(ctx, page, pageSize int, order, query string, args ...any)4. response.Response 的 Data / RequestID 去掉 omitempty(M-38/M-39)
data 与 request_id 字段在所有响应中恒存在(失败时 data:null、未装 RequestID 中间件时 request_id:"")。下游严格按 schema 解析不再缺字段。
5. cache.IsLocked / GetLockTTL / ForceUnlock Redis 不可用改返 ErrRedisNotReady(M-E)
锁操作(正确性相关)Redis 不可用返 ErrRedisNotReady;cache 数据操作(Get/Set/Incr,性能层)保持 best-effort 静默。调用方 errors.Is(err, cache.ErrRedisNotReady) 区分"Redis 不可用"与"锁未占用"。
6. config.Load() 返回深拷贝(M-G)
新增 (*Config).Clone() 深拷贝所有切片字段(CORS/Upload/Storage 白名单);Load() 与 reload 回调返 Clone。Get() 仍返回内部只读指针(热路径零分配),需可变副本用 Clone()。
7. handler.GetPage 加 page 上限 10000(M-D)
防 ?page=999999999 产生超大 OFFSET 拖垮 DB(深分页 DoS)。超过钳制到 MaxPage,需更深遍历改游标/keyset 分页。
8. utils.EqualsIgnoreCase 改 strings.EqualFold(L-C)
原仅 ASCII 字节折叠('A'-'Z' → +32)对非 ASCII(如 É/é)误判为不等;现 Unicode 大小写折叠,行为更正确且更快。
9. utils.ReadFile 去 FileExists 前置检查(M-F)
消除 TOCTOU 竞态,直接 os.ReadFile。文件不存在返 *os.PathError,用 errors.Is(err, os.ErrNotExist) 判断(原字符串 "file not found" 不再返回)。
10. App.Init() 改 sync.Once
多次调用返回首次执行结果(含错误),不再"第二次直接返回 nil"。
11. xlgo.WithConfig(cfg) 不再调用 config.Set(cfg)
配置不再写入全局状态。依赖 config.Get() 取注入配置的下游改用 WithConfigPath(或 NewFullStack)。
12. database.RedisClient 不再可外部访问
包级变量改 unexported。所有消费者用 database.GetRedis();测试注入用 database.SetTestRedisClient(c)。
Bug修复(4 轮评审)
第一轮:CRITICAL + HIGH
- U1
UUIDShort生成错误(保留破折号)→ReplaceAll - M1
filterSensitiveFields假过滤(密码仍可见)→ 编译期正则真抹除 - A1
App.Init并发竞态 →sync.Once - D3/CK1 Redis 客户端访问竞态与不一致 → unexported + 单源
GetRedis - M2 Metrics in-flight gauge 泄漏 →
defer Dec() - R1
FailWithError丢弃 Detail → 走ToResponse
第二轮:并发纪律红线 + 生命周期/泄漏修复
- C-1/H-4
DefaultRedis并发竞态 +redisClient双源 →atomic.Pointer+ 单源 - H-13
validation.Validator无锁读写 →atomic.Pointer - H-11
storage死代码全局 +DefaultStorage裸指针 → 删除 +atomic.Pointer - H-10
response.Error.WithDetail并发不安全 → 返回拷贝 - H-6
RedisRateLimiter.failClosed数据竞争 →atomic.Bool - H-7
GetCSRFToken裸断言 + 非恒定时间比较 → comma-ok +subtle.ConstantTimeCompare - H-14/M-64/M-65
trace.Closesync.Once 泄漏 + Init 回滚不全 → 去sync.Once,Swap+Shutdown - H-8/H-9
ws.Hub.Stopdouble-close panic + WaitGroup Add/Wait 竞态 →stopOnce+runDonechannel - H-1
app.OnReady失败资源泄漏 → 走Shutdown() - H-2
app.Go+Init失败 goroutine 泄漏 → cancel rootCtx + 限时等 wg - H-12
utils.HTTPClient.SetSkipTLS数据竞争 → 写锁下重建 transport+client
第三轮:P0 安全阻断 + P1 并发/资源收口
- #1 JWT 算法混淆(alg confusion)→
WithValidMethods+*jwt.SigningMethodHMAC断言 - #2 JWT 空密钥 fail-closed →
ErrEmptySecret - #3 JWT 不支持算法拒绝 →
ErrUnsupportedAlgorithm - #4 上传大小实测封顶(不信任客户端
file.Size)→enforceUploadSize/enforceMaxReader - #5 HTTP header/cookie map 竞态 → 写锁 +
snapshotHeadersCookies快照 - #6 HTTP SSRF 防护 →
NewSSRFSafeHTTPClient+net.Dialer.Control拦截内网/元数据 IP - #7–#21 P1 并发/资源/泄露收口:console 写锁、config TOCTOU + debounce timer Stop、CSRF body 复原、cron
StopWithTimeout+WithCron()生命周期、databasem.cfg锁内读写、isTransientDBError移除过宽子串、routerapplyOnce+ 排序、logger 查询脱敏、response Detail 门控、validation 密码/手机号强化 +RegisterValidation检错、trace noop +defer span.End()+ 低基数 span 名、storage Abs fail-closed、CLI 硬化(输入校验 + 失败回滚)
第四轮:终审剩余项收口
- H-A 从库连接池
MaxOpenConns/2截断(配置 ≤1 时变 0 无限制)→replicaMaxOpenConns返max(1,/2) - H-B
router.GroupWithMiddlewareGroupnil panic → 改走ensureRegistry() - M-A JWT 黑名单无超时 + 吞错 →
context.WithTimeout(1s)+.Result()显式错误 - M-B
Recover响应已写出时无效写 500 →c.Writer.Written()守卫 - M-C
RedisRateLimiter多次GetRedis()nil-deref 窗口 → 取一次rdb复用 - M-D
handler.GetPage深分页 DoS →MaxPage=10000 - M-E Redis 不可用失败语义统一 →
ErrRedisNotReady(见破坏性变更 #5) - M-F
HashFile/ReadFileOOM 与 TOCTOU → 流式io.Copy+ 去 TOCTOU - M-G
config.Get()切片别名 →Clone()深拷贝(见破坏性变更 #6) - M-H
cron.checkAndRun持锁 spawn 阻塞管理 API → 锁内收集、锁外 spawn - L-A compress 解压残留文件 →
os.Remove - L-B utils 正则重编译 → 包级
regexp.MustCompile - L-C
EqualsIgnoreCase非 ASCII 误判 →strings.EqualFold - L-D
redisLimiters死代码 → 删除 - L-E
logger.Logger导出变量 → 标Deprecated - L-H
ws.SetCheckOrigin无锁写 → 文档约束"仅启动前调用" - L-J
cron.RunTask用s.ctx→ 文档说明 Stop 后行为
未修(评估后决定):L-K model 时间戳 omitempty(time.Time 的 omitempty 是 no-op,报告建议无效,真修需 *time.Time,暂不做);L-I response.writeResp nil-c(nil *gin.Context 是程序员错误,panic 恰当,加守卫反掩盖 bug)。
🔒 安全
- JWT:算法混淆 / 空密钥 / 不支持算法全部 fail-closed(
WithValidMethods+ HMAC 断言 +ErrEmptySecret/ErrUnsupportedAlgorithm)。 - 上传:大小实测封顶(不信任客户端
file.Size),超限清理半截文件。 - HTTP:SSRF 防护(拦截回环/私有/链路本地/元数据 IP,覆盖重定向每一跳);headers/cookies map 并发安全。
- CSRF:token 恒定时间比较(
subtle.ConstantTimeCompare)+ comma-ok 类型断言。 - 鉴权路径:JWT 黑名单 Redis 操作 1s 超时(防 Redis 挂起阻塞每个请求的鉴权)。
- 限流:登录防爆破场景 fail-closed(Redis 故障时拒绝,防限流静默失效)。
📚 文档与脚手架
- 脚手架对齐:
cmd/xlgo的RepositoryMake模板FindByName改用FindOne(走readConn读写分离,避免GetDB不路由的 M-35 footgun);CLI 已硬化(项目名/模块路径校验 + 失败回滚)。 - 文档核验:README/GUIDE 逐行对照源码核验,修复 7 处差异——
- README DB 示例
users := ...Find(&users)自引用声明编译错误 - GUIDE
xlgo.StartServer(engine, 8080)不存在 →engine.Run(":8080") - GUIDE
app.token_expire死键(v1.1.0 已移除AppConfig.TokenExpire) - GUIDE
jwt.expire: 86400类型错(time.Duration需字符串"24h") - GUIDE
storage.Init(&cfg.Storage)吞错误 → 显式 err 处理 - GUIDE 登录示例吞
jwt.GenerateToken错误 +time.Duration双重转换 - GUIDE 验证规则表补
phone_strict/username
- README DB 示例
- CHANGELOG/README 新增 v1.2.0 条目;GUIDE 文档版本同步至 v1.2.0。
升级指南(v1.1.1 → v1.2.0)
go get github.com/EthanCodeCraft/xlgo-core@v1.2.0- 全局搜索旧 API 并按"破坏性变更"章节迁移:
database.DefaultRedis./database.DefaultManager./storage.DefaultStorage.直接方法调用 → facade 或.Load()database.DefaultManager =→database.SetDefaultManager()jwt.DefaultJWT→jwt.GetDefaultJWT()validation.Validator.Struct→validation.ValidateStructdatabase.RedisClient→database.GetRedis()FindWhereOrdered/FindPageWhereOrdered旧签名 → 新签名(order前置,args ...any)
- 若依赖
response.Response的data/request_id在失败时缺失:现在两字段恒存在,按需调整下游 schema 解析。 - 若调用
cache.IsLocked等锁操作:检查 Redis 不可用时的错误处理,改用errors.Is(err, cache.ErrRedisNotReady)。 - 若用
utils.ReadFile判文件不存在:改用errors.Is(err, os.ErrNotExist)。 - 验证:
go vet ./... && go build ./... && go test -race ./...
脚手架生成的项目(
xlgo new)默认只走稳定 facade,不直接引用任何被改类型的符号,开箱即编译通过。
验证
go vet -buildvcs=false ./... # EXIT 0
go build -buildvcs=false ./... # EXIT 0
go test -buildvcs=false -race -count=1 ./... # 全 ok,EXIT 0完整变更历史:见 CHANGELOG.md 的 [1.2.0] 章节。
评审报告:本轮修复依据 deepseek v4 Pro / GLM 5.2 / Claude opus 4.8 三方独立评审 + 终审交叉核验。