Skip to content

v1.2.0

Choose a tag to compare

@EthanCodeCraft EthanCodeCraft released this 04 Jul 04:03

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 / 锁,根除"局部全对、全局全错"的并发安全隐患。

本轮无新增功能,全部为结构性修复与对齐。三条主线闭合:

  1. 包级可变全局保护统一(config/cache/jwt/trace/database/redis/storage/validation/router/response 全部 atomic.Pointer
  2. 资源生命周期在重建路径上彻底释放(trace sync.Once、app OnReady、http transport、ws Hub)
  3. 跨文件失败语义/契约一致(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 []anyargs ...any;因 Go 变长参数须为末尾参数,order 前置于 query

// 新签名
FindWhereOrdered(ctx, order, query string, args ...any)
FindPageWhereOrdered(ctx, page, pageSize int, order, query string, args ...any)

4. response.ResponseData / RequestID 去掉 omitempty(M-38/M-39)

datarequest_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.GetPagepage 上限 10000(M-D)

?page=999999999 产生超大 OFFSET 拖垮 DB(深分页 DoS)。超过钳制到 MaxPage,需更深遍历改游标/keyset 分页。

8. utils.EqualsIgnoreCasestrings.EqualFold(L-C)

原仅 ASCII 字节折叠('A'-'Z'+32)对非 ASCII(如 É/é)误判为不等;现 Unicode 大小写折叠,行为更正确且更快。

9. utils.ReadFileFileExists 前置检查(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.Close sync.Once 泄漏 + Init 回滚不全 → 去 sync.Once,Swap+Shutdown
  • H-8/H-9 ws.Hub.Stop double-close panic + WaitGroup Add/Wait 竞态 → stopOnce + runDone channel
  • 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() 生命周期、database m.cfg 锁内读写、isTransientDBError 移除过宽子串、router applyOnce + 排序、logger 查询脱敏、response Detail 门控、validation 密码/手机号强化 + RegisterValidation 检错、trace noop + defer span.End() + 低基数 span 名、storage Abs fail-closed、CLI 硬化(输入校验 + 失败回滚)

第四轮:终审剩余项收口

  • H-A 从库连接池 MaxOpenConns/2 截断(配置 ≤1 时变 0 无限制)→ replicaMaxOpenConnsmax(1,/2)
  • H-B router.GroupWithMiddlewareGroup nil 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/ReadFile OOM 与 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.RunTasks.ctx → 文档说明 Stop 后行为

未修(评估后决定):L-K model 时间戳 omitemptytime.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/xlgoRepositoryMake 模板 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
  • CHANGELOG/README 新增 v1.2.0 条目;GUIDE 文档版本同步至 v1.2.0。

升级指南(v1.1.1 → v1.2.0)

  1. go get github.com/EthanCodeCraft/xlgo-core@v1.2.0
  2. 全局搜索旧 API 并按"破坏性变更"章节迁移:
    • database.DefaultRedis. / database.DefaultManager. / storage.DefaultStorage. 直接方法调用 → facade 或 .Load()
    • database.DefaultManager = database.SetDefaultManager()
    • jwt.DefaultJWTjwt.GetDefaultJWT()
    • validation.Validator.Structvalidation.ValidateStruct
    • database.RedisClientdatabase.GetRedis()
    • FindWhereOrdered / FindPageWhereOrdered 旧签名 → 新签名(order 前置,args ...any
  3. 若依赖 response.Responsedata/request_id 在失败时缺失:现在两字段恒存在,按需调整下游 schema 解析。
  4. 若调用 cache.IsLocked 等锁操作:检查 Redis 不可用时的错误处理,改用 errors.Is(err, cache.ErrRedisNotReady)
  5. 若用 utils.ReadFile 判文件不存在:改用 errors.Is(err, os.ErrNotExist)
  6. 验证: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 三方独立评审 + 终审交叉核验。