Releases: EthanCodeCraft/xlgo-core
Release list
v1.4.0
v1.4.0 - instance+facade 一致性收尾
把 cron/storage/cache/ratelimit 四个"纯全局"包按
database.Manager/jwt.Manager模式实例化,trace 纳入 App 生命周期,清理WithoutWire技术债。单进程多 App 部署成为一等公民。
为什么发这个版本
database.Manager/logger.LogManager/config.Manager 早已是"实例 + 全局默认 facade"模式,但 cron/storage/cache/ratelimit 仍是纯全局--App 不持 per-App 实例,只绑包级全局 facade。后果:
- 多 App 互相污染:App A
Shutdown会停掉 App B 的 cron 调度器与限流器;App BInit覆盖 App A 的 storage driver;cache/ratelimit 硬编码database.GetRedis(),多 App 下 A 的缓存读到 B 的 Redis。 - trace 未纳生命周期:
trace.Close从未被调用,OTel exporter 后台 goroutine/连接泄漏。 WithoutWire空函数技术债:v1.1.0 删 wire 包后遗留的 no-op,违反框架"不保留 deprecated wrapper"纪律。
本版本收尾这四包的实例化,补齐 trace 生命周期,并修复连带的多 App 跨 App 误杀问题。
⚠️ 破坏性变更(升级前必读)
1. xlgo.WithoutWire() 删除
v1.1.0 删 wire 包后遗留的空 no-op,全仓无引用。直接删除调用即可,无行为变化。
2. cron.AddTask(...) 在 App.Init 之前注册不再生效
App 现持专属调度器,Init 时创建并提升为全局默认。pre-Init 调包级 cron.AddTask 会注册到 init 默认调度器、被 App swap 丢弃。
// ❌ 旧(pre-Run 全局注册,v1.4.0 失效)
cron.AddTask("cleanup", cron.Every(5*time.Minute), cleanup)
app := xlgo.New(xlgo.WithConfig(cfg), xlgo.WithCron())
// ✅ 新(WithCronTask 注册到 App 调度器,蕴含启用 cron)
app := xlgo.New(xlgo.WithConfig(cfg),
xlgo.WithCronTask("cleanup", cron.Every(5*time.Minute), cleanup),
)post-Init 的包级 cron.AddTask 仍可用(代理到 App 调度器);standalone(无 App)用法不变。
3. middleware.StopRateLimiters() 语义收窄
仅停默认 Registry 的限流器。App Shutdown 改调自己的 registry.Stop(),不再误停其他 App。
4. commitReplacedResources 不再关闭 previousDB/previousRedis/loggerSnapshot
多 App 下 "previous" 可能是另一个 App 的活跃资源,关闭致 redis: client is closed 或 logger writer 误杀。单 App 无影响(init 默认无资源)。手动 database.InitRedis 后再 App.Init 的旧 client 关闭由用户负责。
5. cache/ratelimit redis 注入(签名扩展)
cache:新增NewRedisCacheWithRedis(client)/CacheManager.InitWithRedis(client)/SwapDefaultCacheManager(m);NewRedisCache()仍存在(standalone)。middleware:RedisRateLimiter新增WithRedisClient(client)选项。
✨ 新增
WithTrace - trace 纳入 App 生命周期
app := xlgo.New(xlgo.WithConfigPath("config.yaml"), xlgo.WithTrace())# config.yaml
trace:
enabled: true
service_name: my-service
endpoint: localhost:4318
exporter_type: otlp-http # otlp-http / otlp-grpc / stdout
sample_ratio: 1.0
# insecure: true # 明文 collector 需开启
# propagator: w3c # w3c(默认) / b3 / jaegerInit 调 trace.Init + 装 trace.Middleware;Shutdown 调 trace.Close(此前从未调用,致 exporter 泄漏)。不做实例隔离--OTel TracerProvider 是进程级全局单例,多 App 不要同时开 WithTrace。
App-bound 限流中间件(多 App per-App 计数隔离)
// 包级 LoginRateLimit() 多 App 下仅绑最后 Init 的 App;
// 用 app-bound 实现真正的 per-App 计数隔离:
app.GetRouter().GET("/login", app.RateLimitRegistry().LoginRateLimit(), handler)新增 App getter(绕过全局 facade,直取 App 实例)
app.Scheduler()- cron 调度器app.Cache()- 缓存服务app.RedisClient()- Redis 客户端(注入RedisRateLimiter等)app.RateLimitRegistry()- 限流器 Registry
其它
cron/storage/cache/middleware各SwapDefault*(App Init 失败回滚用)config.TraceConfig+Config.Trace字段storage.StorageManager.Close()- 闭合 Init/Close 生命周期(io.Closer断言,当前 no-op,为未来驱动预留收口点)
🐛 修复
- 多 App cron 隔离:App A
Shutdown不再停掉 App B 的调度器。 - 多 App storage/cache 隔离:App B
Init不再覆盖 App A 的 driver / cache redis;cache 不再硬编码全局 Redis。 - 多 App 限流器:App A
Shutdown不再重置 App B 的 loginLimiter 计数(原StopRateLimiters全局置 nil 致 B 下次请求懒创建新 limiter、计数清零,稳态客户端借 A 的 Shutdown 窗口绕过限流)。 - trace exporter 泄漏:
App.Shutdown现调trace.Close。 - 多 App logger/redis 跨 App 误杀:
commitReplacedResources不再关闭非己所有的 previous 资源。
多 App 边界(使用前知悉)
| 项 | 说明 |
|---|---|
| 包级限流 facade | LoginRateLimit 等多 App 下仅绑最后 Init 的 App;per-App 计数隔离用 app.RateLimitRegistry() |
CustomRateLimit(包级) |
首请求须在 Init 后(否则 cleanup goroutine 泄漏);app-bound 路径无此问题 |
| trace | 进程全局共享,多 App 任一 Shutdown 关全局导出 |
StorageManager.Close |
当前 no-op(oss.Client 无 Close),未来驱动实现 io.Closer 即自动收口 |
✅ 验证
go build+go vet+go test -race ./...全绿。- 新增契约测试:trace lifecycle、storage/cron/cache/ratelimit 实例隔离 + 多 App 不串 + rollback 还原、cache 多 Redis 隔离、ratelimit 多 App Shutdown 不重置计数 + per-App 计数隔离、storage Close 各路径。
- 独立对抗性复审:无 CRITICAL/HIGH,发现项全部处置。
升级
go get github.com/EthanCodeCraft/xlgo-core@v1.4.0v1.0.3
同义可失败 API 收敛 + 注释/文档一致性修复 + 前序评审收口。
API 收敛(
gpt_clear_function.md计划):按"同一能力只保留一个主入口、框架不替上层吞错、安全路径默认 fail-closed"原则,收敛 cache/jwt/storage/ratelimit 四个包的历史双轨 API。自定义CacheService/Storage实现需同步更新签名(编译失败即迁移信号)。注释/文档一致性(
glm_note_report.md报告):A/B/C 三档共 23 项注释/文档修复,无运行逻辑变更。前序评审收口:M13 cron panic、M1 App 生命周期状态机、M3 logger 生命周期临界区;config 模块评审(H-config-1 Set/viper 同源、M-config-1 回调 panic 隔离、M-config-2 DB SSL/TLS、M-config-3 App 关闭停 watcher、M-config-4 Clone 守卫);database 模块评审(H-db-1 后台探活/启动 ping 经 pingWithTimeout 3s 约束)。
项目处于初级阶段,无下游用户,放心引入破坏性变更。
go vet+go build+go test -race ./...全绿。
Breaking ⚠️
-
PostgresDSN 默认 sslmode 由
disable改为prefer(M-config-2):原PostgresDSN硬编码sslmode=disable,生产 DB 流量明文。新增DatabaseConfig.SSLMode字段,空值默认prefer(优先加密、失败回退明文),可显式配置disable/allow/prefer/require/verify-ca/verify-full(非法值在Validate阶段报错)。依赖明文 Postgres 连接的下游若因prefer回退行为受影响,请显式设置ssl_mode: disable。MySQL 不受影响(用TLS/TLSRootCA)。 -
cache 写操作与计数器/原始 Redis helper 在 Redis 未初始化时返回
ErrRedisNotReady:Set/Delete/DeleteByPattern/Incr/IncrBy/Decr/GetTTL/SetExpire/GetRaw/SetRaw旧行为会静默返回成功或零值,调用方容易误判缓存写入、计数器更新或过期时间设置已经生效;现在统一显式返回错误。公共接口签名不变,但依赖“未启用 Redis 时当作成功”的下游需要改为忽略errors.Is(err, cache.ErrRedisNotReady)或显式启用 Redis。 -
cache.WithLock/cache.WithLockAutoExtend未获取到锁时返回ErrLockNotAcquired:旧行为返回nil并跳过业务函数,调用方无法区分“业务执行成功”和“根本没有执行”。同时锁 TTL 小于 1ms、续期/重试间隔非正会返回显式错误,避免 Redis PX=0 或time.NewTicker(0)崩溃。 -
cache.WithLock/cache.WithLockAutoExtend的业务函数签名改为func(context.Context) error:旧签名func() error无法强制业务函数接收取消信号,容易在请求取消/超时后继续访问 DB/HTTP 等下游资源;现在框架会把调用方 ctx 传入业务函数。nil 业务函数返回新增ErrLockFuncNil。 -
test.Request.Execute()改为返回*test.Response:旧返回值是*httptest.ResponseRecorder,与文档示例中的resp.AssertOK(t)/resp.ParseJSON(...)不一致;现在Execute()返回带断言和 JSON 解析方法的包装类型。需要原始 recorder 的调用方改用新增ExecuteRecorder()。 -
config.Get()/(*config.Manager).Get()改为返回配置副本:旧行为暴露内部*Config,调用方修改返回值会污染全局配置并可能与热重载并发读写竞态;现在返回深拷贝。需要动态替换配置的测试或工具代码请改用config.Set(cfg)。 -
config.Set()/(*config.Manager).Set()现在返回error:非 nil 配置会先执行Validate(),非法配置不会覆盖旧配置,并返回ErrInvalidConfig包装错误。旧代码可以继续忽略返回值,但建议测试和启动路径显式检查。 -
config.GetViper()/(*config.Manager).GetViper()改为返回 viper 快照:旧行为暴露内部可变*viper.Viper;现在修改返回对象不会影响全局配置。常规读取请使用GetString/GetInt/GetBool/GetStringMap。 -
未知数据库 driver 不再静默回退 MySQL:
config.DatabaseConfig.DSN()对非空但未注册的driver返回空字符串,database.Dialector返回初始化即失败的 Dialector;空driver仍保持默认 MySQL。下游若使用自定义数据库驱动,需先通过database.RegisterDialect或config.RegisterDSNBuilder注册。 -
database.InitDB/(*database.Manager).InitDB/InitDBWithReplicas必须显式传入context.Context:旧 API 无法取消初始化过程中的 Ping 与重试等待,shutdown 或启动失败回滚时可能长时间卡住。现在调用方必须传入生命周期 ctx;普通测试或一次性脚本可使用context.Background(),App 初始化会使用 App root ctx。 -
database.SetDefaultManager/database.SetDefaultRedisManager会关闭被替换的旧 manager:旧行为只做 atomic 替换,直接调用会遗留旧 DB/Redis 连接池。现在普通 Set 表示“接管全局默认资源并释放旧资源”;需要失败回滚或延迟释放旧资源的初始化流程请改用新增database.SwapDefaultManager/database.SwapDefaultRedisManager。 -
jwt.ParseToken开始校验 issuer,RefreshToken使用jwt.refresh_expire:签发者与当前配置不一致的 token 会被拒绝;刷新后的 token 过期时间优先使用refresh_expire,未配置时回退expire。GenerateTokenWithCustomExpiry现在拒绝非正过期时间,InvalidateTokenByID("")返回ErrEmptyJTI。 -
App.Init()由sync.Once改为生命周期状态机(app.go,M1):5 态stateCreated/Initializing/Initialized/Stopping/Stopped+lifecycleMu(RWMutex) +initMu(Mutex)。Shutdown后或Init失败后再调Init()返回新增导出错误xlgo.ErrAppClosed(原sync.Once"多次调用返回首次结果"语义不再适用——已关闭的 App 不可再 Init,需新建 App)。 -
App.Go()在 Shutdown 开始或 Init 失败后为 no-op(app.go,M1):state >= stateStopping时拒绝wg.Add直接返回,避免与Shutdown的wg.Wait竞争sync.WaitGroup契约(Add 须 happen-before Wait)。依赖"Shutdown 后仍可 Go"的下游需改用独立 goroutine。 -
生命周期 hook 不允许重入调用
Init/Shutdown/Run(app.go,M1):initMu非重入,hook(OnInit/OnStart/OnReady/OnStop)内调用会自锁死锁。需在 hook 内触发关闭应改用信号通道由主流程处理。 -
xlgo.WithConfig(cfg)改为快照语义并在Init时校验(app.go,M1):传入配置会深拷贝到 App 私有快照,调用方后续修改原cfg不再影响 App;非法配置在Init返回中文校验错误。依赖“修改原 cfg 指针动态影响 App”的下游需改为重新创建 App 或使用配置管理器。 -
logger.DefaultLogger = m直接赋值不再驱动包级 facade(logger/logger.go,M3):为消除SetDefaultLogManager()与包级 facade 并发读取默认 manager 的裸全局指针竞态,facade 改为读取内部 atomic 快照。logger.DefaultLogger仍保持*LogManager类型,旧的logger.DefaultLogger.Init/Close/SetLevel/GetLevel直接调用仍可用;替换默认 manager 请使用logger.SetDefaultLogManager(m)。 -
logger.Init拒绝明显非法日志配置(logger/logger.go,M3):空日志目录、负数MaxSize/MaxBackups/MaxAge现在直接返回错误。依赖零值日志配置启动WithLogger()的下游需显式设置Log.Dir与非负轮转参数。 -
限流器非法配置改为 fail-fast(middleware/ratelimit.go,M8):
NewRateLimiter/NewRedisRateLimiter/NewRedisRateLimiterFailClosed现在对rate <= 0或window <= 0直接 panic,避免零值窗口/零值配额静默产生不确定限流语义。下游应在配置加载阶段校验限流参数。 -
handler.BindJSON默认限制 JSON body 为 1MiB(handler/handler.go,M6):防止入口层无上限读取请求体导致 OOM。需要更大 JSON 的接口请改用handler.BindJSONWithMaxBytes(c, req, maxBytes)显式声明上限。 -
cron 非法任务配置改为 fail-fast(cron/cron.go,M13):
AddTask拒绝 nil schedule / nil handler;Every(<=0)、Daily/Weekly越界时间、非法 weekday 会 panic,避免静默生成不推进或归一化跑偏的调度。 -
cron.ParseCron非法表达式改为 fail-fast panic(cron/cron.go,M13):旧行为会把非法表达式静默回退为每分钟执行,容易让拼写错误变成高频任务。动态输入请用ParseCronStrict处理 error;确实需要旧回退语义时改用新增ParseCronOrDefault。 -
repository 查询保护默认开启(repository/repository.go,M5/N5):
FindAll默认最多返回DefaultFindAllLimit=1000条;明确需要全表扫描时改用FindAllUnbounded。FindPage*/QueryBuilder.Page会归一化page/pageSize并限制MaxPageSize=100、MaxPage=10000。Find*Ordered/QueryBuilder.Order只接受简单字段排序(如created_at DESC, id ASC),复杂表达式/raw SQL 会返回ErrUnsafeOrder;UpdateBatch字段名不合法返回ErrUnsafeField。 -
同义可失败 API 收敛:错误不再吞并(cache/jwt/storage/ratelimit):按"同一能力只保留一个主入口、框架不替上层吞错、安全路径默认 fail-closed"原则,收敛历史双轨 API。详见下述分项;自定义
CacheService/Storage实现需同步更新签名(编译失败即迁移信号)。-
cache:
CacheService.Get/Exists改为(bool, error)--命中(true,nil)、未命中(false,nil)、Redis 未就绪/命令错误/反序列化失败返回(false,err)。删除cache.GetE/cache.ExistsE/CacheGetter/CacheExistChecker/redisCache.GetE/redisCache.ExistsE(不保留 deprecated wrapper)。新增包级cache.Get(ctx,key,dest) (bool,error)/cache.Exists(ctx,key) (bool,error)。GetWithPrefix改为(bool,error)。迁移:hit, err := cache.Get(ctx, key, &v); if err != nil { return err }; if !hit { /* miss */ }。 -
jwt:
TokenBlacklist.IsBlacklisted改为(bool, error),删除IsBlacklistedE。IsTokenRevoked改为(bool, error)。ParseToken默认从 fail-open 改为 fail-closed--黑名单后端不可检查时返回ErrBlacklistUnavailable拒绝该 Token(无 Redis 部署不再支持可靠撤销)。删除ParseTokenFailClosed(主 API 已 fail-closed)。保留ParseTokenWithBlacklistPolicy(token, policy)+BlacklistPolicy/BlacklistFailOpen/BlacklistFailClosed供显式 fail-open(仅无 Redis 或低安全场景)。迁移:无 Redis 部署需启用 Redis,或显式jwt.ParseTokenWithBlacklistPolicy(token, jwt.BlacklistFailOpen)。 -
storage:
Storage.Exists改为(bool, error)--存在(true,nil)、不存在(false,nil)、未初始化/路径非法/穿越/后端错误返回(false,err)。LocalStorage.Exists区分os.IsNotExist(not found)与其他os.Stat错误;OSSStorage.Exists区分 OSS 404/NoSuchKey(not found)与鉴权/网络错误。包级storage.Exists同步签名,未初始化返回ErrStorageNotInitialized。新增ErrReadTooLarge:LocalStorage.Get/OSSStorage.Get读取超maxReadBytes上限原误用ErrInvalidPath(路径无效语义不贴切),改为ErrReadTooLarge。迁移:ok, err := storage.Exists(p); if err != nil { return err }; if !ok { /* not found */ }。 -
ratelimit:Redis 限流器策略收敛为配置型 API。
NewRedisRateLimiter(keyPrefix, rate, window, opts ...RedisRateLimiterOption)新增可变参数,WithFailClosed(true)替代原NewRedisRateLimiterFailClosed。RedisRateLimit/CustomRedisRateLimit/RedisRateLimitWithIdentifier同步加opts参数。删除NewRedisRateLimiterFailClosed/RedisRateLimitFailClosed/CustomRedisRateLimitFailClosed。UploadRedisRateLimit由 fail-open 改为 fail-closed(上传属资源敏感操作,Redis 故障时拒绝以防限流静默失效)。迁移:middleware.RedisRateLimit("k", 100, middleware.WithFailClosed(true))替代原RedisRateLimitFailClosed("k", 100)。
-
Security 🔒
- MySQL 连接支持 TLS(M-config-2):
DatabaseConfig.TLS为 true 时MySQLDSN追加tls=true(go-sql-driver/mysql v1.7.0 内置安全语义:系统根 CA + ServerName 自动取自 Host + 证书校验,无需注册)。配合DatabaseConfig.TLSRootCA(PEM 路径)可指定私有 CA/自签证书,由database包在InitDB时RegisterTLSConfig注册命名配置(config.MySQLTLSConfigName);CA 不可读或非 PEM 时 fail-fast,不静默回退明文。 - Postgres 连接支持 sslmode 配置(M-config-2):见 Breaking 项,默认
prefer优先加密。
Fixed 🐛
- config
Set(cfg)与 viper 视图同源修复(H-config-1):Set原只更新类型化视图m.cfg,m.v(viper)停留旧值,导致Get()与GetString/GetInt/GetBool/GetViper返回不同世界(违反 C1 单一配置源)。现在Set用 mapstructure 将*Config重建为不含AutomaticEnv的 viper 视图,保证 Get 与 GetString 同源。 - config 热重载回调 panic 隔离(M-config-1):单个
onChange回调 panic 原会传播致 watcher 泄漏、后续热更新静默失效。现在每个回调独立recover(标准库log记录 + 堆栈),不阻断后续回调、不杀 watcher。 - App.Shutdown 停止 configManager watcher(M-config-3):
closeResources末尾新增configManager.StopWatcher(),用户对 App 的 configManager 调LoadWithWatch/StartWatcher后由 Shutdown 统一收口,避免关闭后遗留监听 goroutine(违反 C7)。 - config
Clone切片字段覆盖守卫(M-config-4):新增反射测试枚举Config所有切片/map 字段,断言Clone深拷贝;新增切片字段未同步 fixture 或Clone时测试失败,形成机械守卫。 - config
watchLoop增加 ctx 逃生通道(L-config-2):原仅靠w.Events关闭退出,与"for 消费循环须 ctx.Done"红线有张力。StopWatcher改为 cancel ctx + Close watcher 双重退出。 - config
Validate连接池交叉校验(L-config-3):MaxOpenConns>0时MaxIdleConns>MaxOpenConns视为配置错误。 - config 哨兵错误改用
errors.New(L-config-4);DSN()去除冗余 TrimSpace(L-config-5);DSN/MySQLDSN/PostgresDSN/Addrnil receiver 防御(L-config-6)。 - database 后台探活/启动 ping 经 pingWithTimeout 3s 约束(H-db-1,M11 修复不完整):
pingWithTimeout原只加到包级HealthCheck(),未覆盖方法(*Manager).HealthCheck(被后台探活probeOncemaster 与/health端点共用)、probeOnce从库 ping、InitDB/InitDBWithReplicas启动 ping。挂起 DB(连接活但不响应)下这些路径的PingContext无 ctx deadline 无限阻塞,...
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 三方独立评审 + 终审交叉核验。
v1.1.1
v1.1.1
v1.1.0 的补丁发布:补
ServerConfig.Host字段、统一面向用户文案为中文、修正 README 过时/错误描述。
Added ✨
ServerConfig.Host(绑定地址)
server 新增 host 字段控制监听地址:
host: ""(默认)→:8080,监听所有接口(0.0.0.0),向后兼容host: "127.0.0.1"→ 仅本机(前面有 nginx 时常用)host: "10.0.0.5"→ 绑定内网网卡
避免生产环境无意暴露在 0.0.0.0。
Changed 🔄
面向用户文案统一中文
v1.1.0 前部分面向用户/调用的文案为英文,与其余中文不一致。本次统一为中文:
middleware/recover.go:"Panic recovered"→"panic 已恢复";"Panic: %v"→"服务器内部错误: %v"middleware/logger.go:5 处日志消息改中文middleware/metrics.go:3 个 PrometheusHelp改中文app.go/database/manager.go/logger/logger.go:英文 error 改中文
保留英文:JSON 字段名、health 状态枚举、Prometheus metric Name、MySQL 错误串匹配、技术专有名词。
Fixed 🐛
README 错误描述修正
v1.1.0 后 README 存在过时/错误描述,照抄会导致新用户启动失败:
- 删除目录结构里已移除的
wire/段 - 快速开始配置示例:
jwt.secret补足 ≥32 字节(否则被Validate拦截启动失败);expire: 86400(int 秒)改为expire: "24h"(time.Duration),补refresh_expire/issuer/algorithm server段补host/timeout/response_mode字段- v1.0.2 更新日志标注
WithWire已移除 - 目录结构补 v1.1.0 新文件(metrics/timeout/validate/mode 等)
- 框架特性段重写,补全 v1.1.0 能力
- 响应格式段补
Mode开关与CustomAPI
升级说明 🛠️
从 v1.1.0 升级无破坏性变更,host 字段默认空,行为与 v1.1.0 一致。
升级命令:go get github.com/EthanCodeCraft/xlgo-core@v1.1.1
完整变更见 CHANGELOG.md。
v1.1.0
v1.1.0 — HA & Manager 化
本版本定位为 HA & Manager 化 release:高可用与生产就绪改进 + 组件 Manager 化。对应体检报告 #10-#24。
⚠️ 含少量破坏性变更,升级前请阅读「升级说明」。
Breaking ⚠️
- 删除
wire包及WithWireOption(WithoutWire保留空 stub 兼容) - 删除
AppConfig.TokenExpire(与JWTConfig.Expire重复) JWTConfig.Expire由int(秒)改为time.Duration("24h")- 删除
StartServerWithPort与GracefulShutdown双轨函数
Added ✨
组件 Manager 化(#10)
storage / cache / redis / jwt / logger 五组件新增 XxxManager + DefaultXxx + SetDefaultXxxManager,包级 facade 保留兼容存量。支持多实例与测试注入 mock。
Lifecycle Hooks(#12)
WithHook(Hook{OnInit/OnStart/OnReady/OnStop}),覆盖 Init/启动/就绪/关闭各阶段。
App.Go + in-flight goroutine(#22)
App.Go(func(ctx)) 启动受管理的后台 goroutine,Shutdown 时 cancel + wg.Wait(带超时)。
Server 参数配置化(#13)
server 新增 read_timeout/write_timeout/idle_timeout/shutdown_timeout/max_header_bytes/tls/unix_socket/response_mode,支持 TLS 与 unix socket。
JWTConfig time.Duration(#14)
jwt.expire/refresh_expire 用 Duration("24h"/"168h"),新增 issuer/algorithm。
Config Validate(#16)
Config.Validate() 在 Manager.Load 后自动调用,启动期拦截非法配置。
response REST 模式(#15)
response.SetMode(ModeBusiness|ModeREST),默认兼容存量;ModeREST 按错误码映射 HTTP status。新增 response.Custom()。
livez / readyz(#17)
/livez(存活,始终 200)+ /readyz(就绪,依赖检查 503),K8s probe 友好。
Prometheus metrics(#18)
/metrics 端点 + middleware.Metrics() 采集 http_requests_total/http_request_duration_seconds/http_requests_in_flight。
请求级 Timeout 中间件(#19)
middleware.Timeout(d),下游走 c.Request.Context() 级联取消。
依赖健康自愈(#21)
主库后台探活 + replica 健康剔除,/readyz//health 联动 503。新增 conn_max_idle_time/health_check_interval/health_check_failure_threshold 配置。
RequestID 默认装入(#24)
App.Init 无条件装入 RequestID()(Recovery 前),响应/panic 日志均带 request_id。移除 gin.Recovery() 双重保险。
升级说明 🛠️
- wire 包删除:移除
wireimport 与wire.InitServices()/WithWire()调用。cache.Init()现由WithRedis自动触发。 - AppConfig.TokenExpire 删除:改用
jwt.expire。grep 清理token_expire。 - JWTConfig.Expire 类型变更:YAML
expire: 86400→expire: "24h";代码time.Duration(cfg.JWT.Expire) * time.Second→cfg.JWT.Expire。 - StartServerWithPort / GracefulShutdown 删除:改用
App.Run()/App.Shutdown()。 - JWT 密钥长度:Validate 要求启用 JWT 时 secret ≥32 字节,短密钥启动期被拦截。
- 配置文件:
jwt.expire必须改为 Duration 字符串;server.read_timeout等可选(缺省回退)。
升级命令:go get github.com/EthanCodeCraft/xlgo-core@v1.1.0
完整变更见 CHANGELOG.md。
v1.0.4
v1.0.4 — DX & Docs Release
定位:开发体验与文档改进,无破坏性 API 变更。对应体检报告 #25/#27/#28/#29/#30。
完整变更说明见 CHANGELOG.md。
✨ Added
CLI 多模板(#28)
xlgo new 新增 --template 参数,支持三种脚手架模板:
xlgo new myapp --template minimal # 轻量 HTTP,无 MySQL/Redis 依赖
xlgo new myapp --template api # 标准业务 API,含分层目录(默认)
xlgo new myapp --template fullstack # 全组件,NewFullStack 一键启用| 模板 | 说明 | 适用场景 |
|---|---|---|
minimal |
仅 logger + health + 示例路由,目录最小化 | 第一次接触 xlgo、纯 HTTP 服务 |
api |
handler/model/repository/service 分层 + MySQL/Redis/JWT 配置 | 标准业务 API(默认) |
fullstack |
NewFullStack 全组件 + Swagger + Storage |
全功能应用 |
examples/ 目录(#29)
新增两个可运行示例,帮助快速上手:
examples/minimal/— 50 行可跑,不依赖外部服务go run ./examples/minimal
examples/full/— MySQL + Redis + JWT + user CRUD 完整示例(登录发 token、认证路由、创建/查询用户)examples/README.md— 运行说明与接口文档
docs/ 文档结构(#30)
- 新增
docs/目录,docs/plans/归档历史规划与体检报告 - 新增
docs/README.md文档索引 - 归档:
Version_Update_Plan_v1.0.2.md、Version_v1.0.2_report.md、早期report.md(→v2.0-review.md) CHANGELOG.md/GUIDE.md按惯例保留在仓库根目录
🔄 Changed
模块路径文档改进(#25)
经评估保留 xlgo-core 模块路径——-core 后缀反映这是 xlgo 多产品系列(xlgo-core / xlgo-orm / xlgo-ai ...)的核心产品,不去掉。模块路径(github.com/EthanCodeCraft/xlgo-core)与包名(xlgo)不同是 Go 惯例(cf. github.com/gin-gonic/gin → 包名 gin)。
改进文档说明,消除新用户 go mod tidy 撞墙的困惑:
- README 快速开始新增「模块路径与包名」小节,给出完整 import 示例:
import xlgo "github.com/EthanCodeCraft/xlgo-core"
- CLAUDE.md
Import Path Note措辞明确化,说明 module path / package name /-core语义
Without* Option 定位文档化(#27)
经调研 Without* 系列 Option 有真实用例(测试覆盖「先开再关」语义 + NewFullStack 后排除单项),不删除、不标 Deprecated,改为文档化其定位:
app.goWithoutLogger注释说明:Without*主要用于NewFullStack/RunFullStack启用全部组件后排除个别项- README 用法示例:
xlgo.NewFullStack(xlgo.WithoutSwaggerRoutes())全组件但关 Swagger
升级说明
v1.0.4 无破坏性变更,从 v1.0.3 升级只需:
go get github.com/EthanCodeCraft/xlgo-core@v1.0.4
go mod tidyFull Changelog: v1.0.3...v1.0.4
v1.0.3
v1.0.3 — Bug Fix Release
定位:bug fix release。收口 v1.0.2 引入的破坏性清理,并修复 4 个轻量 bug + 依赖复查 + 版本号治理 + 文档对齐。
完整变更说明见 CHANGELOG.md。
⚠️ Breaking Changes(升级前必读)
1. 错误码体系重构(response 包)
修复 CodeSuccess 与 CodeInvalidParams 撞码的生产级 bug(两者都等于 1,导致业务错误响应被前端误判为成功)。
| 常量 | 旧值 | 新值 |
|---|---|---|
response.CodeSuccess |
1 |
0 |
response.CodeFail |
0 |
1 |
移除:response.CodeInvalidParams、response.ErrInvalidParams
迁移:
// ❌ 旧(编译失败)
response.FailWithError(c, response.ErrInvalidParams)
// ✅ 业务侧自定义错误码
var ErrInvalidParams = response.NewError(40001, "参数错误")
response.FailWithError(c, ErrInvalidParams)
// ✅ 或直接通用失败
response.Fail(c, "用户名格式错误")前端:if (resp.code === 1) → if (resp.code === 0)
新增 _errorCodeUniquenessGuard 编译期防撞码 map,任何后续 Code* 常量重复都会在 go build 阶段直接报错。
2. 清理 v1.0.2 兼容别名(database 包)
// ❌ 移除
database.InitMySQL(cfg)
database.InitMySQLWithReplicas(cfg, replicas)
(*Manager).InitMySQL / InitMySQLWithReplicas
// ✅ 改用(驱动由 cfg.Database.Driver 决定)
database.InitDB(cfg)
database.InitDBWithReplicas(cfg, replicas)3. CORS 中间件行为收紧(Security 修复)
Access-Control-Allow-Credentials 不再永远是 true。如果你之前依赖"默认允许凭证"的隐式行为,需显式启用:
cors:
allowed_origins: ["https://your-frontend.example"]
allow_credentials: true🐛 Fixed
- JWT JTI 生成忽略
rand.Read错误(jwt)—generateJTI()改为(string, error),GenerateToken/GenerateTokenWithCustomExpiry传播错误 QueryBuilder.Page统计行数被残留 Limit 截断(repository)— countDB 加.Limit(-1).Offset(-1)清除残留分页条件- OSS / 本地存储文件名冲突(storage)— 新增
uniqueFilename(now, ext)(<unixNano>-<8字节crypto/rand hex>.<ext>),4 处上传路径统一改用 - 数据库重试策略对不可恢复错误无效(database)— 新增
isTransientDBError,Access denied/Unknown database/invalid DSN/unknown driver等首次出现即返回,不再无意义重试 5 次
🔒 Security
CORS 中间件按 W3C 规范修复:
Access-Control-Allow-Credentials只在显式启用且 Origin 非*时才发送*+credentials: true场景回显具体 Origin(避免被浏览器拒绝)- 自动加
Vary: Origin,防止 CDN/网关缓存串扰 - 非白名单 Origin 不再被回显
📦 Dependencies
-
go mod tidy补全 postgres 方言传递依赖:jackc/pgx家族、golang.org/x/sync;gorm.io/driver/postgres提升为直接依赖 -
安全补丁升级(无 API 变更):
依赖 旧 新 golang.org/x/cryptov0.49.0 v0.53.0 golang-jwt/jwt/v5v5.2.1 v5.3.1 gorilla/websocketv1.5.1 v1.5.3 -
暂缓升级(留待 v1.0.4 / v1.1):
ginv1.9→v1.12、validatorv10.19→v10.30、gormv1.25→v1.31、aliyun-oss-go-sdkv2→v3(major 破坏性)等
🗑️ Removed
- 死代码
database.DBResolver(BeforeQuery从未注册到 GORM callback chain)。需要 callback 级自动读写分离请接入官方gorm.io/plugin/dbresolver - 292 行"评分/理由"自夸注释(23 个文件)— AI 生成遗留,对外发布不专业
✨ Added
- console 包显式 level 控制:
SetLevel/WithLevel/LevelSilent,atomic.Int32并发安全。console 是开发期彩色 stdout 工具,不写文件、不感知环境 - 新增测试:jwt / repository / storage / database / router / middleware / app
- 文档:新增
CHANGELOG.md、Version_v1.0.2_report.md;更新 README / GUIDE 顶部更新日志
🔄 Changed
- 文件重命名
database/mysql.go → database/manager.go(导入路径无变化,公开 API 全部保留) - Logger 拆分三独立 core 修复 Tee 重复写入:
Logger→app.log、APILog()→api.log、DBLog()→database.log,互不串扰。新增logger.Close(),生产默认级别调整为Info升级注意:日志目录会新增
logs/app.log,运维采集脚本如有需要请补上 - 版本号常量化:
app.go新增const Version作为唯一版本号来源,CLI / 脚手架模板引用之,发版只需改一处
升级速查
- 全局替换前端
code === 1(成功)→code === 0 - 移除
response.ErrInvalidParams/CodeInvalidParams引用,改业务自定义 database.InitMySQL*→database.InitDB*- 检查 CORS 配置:若依赖凭证,显式设
allow_credentials: true - 运维:日志采集补
logs/app.log
go get github.com/EthanCodeCraft/xlgo-core@v1.0.3
go mod tidyFull Changelog: v1.0.1...v1.0.3