Skip to content

v1.0.3

Choose a tag to compare

@EthanCodeCraft EthanCodeCraft released this 12 Jul 14:49

同义可失败 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 未初始化时返回 ErrRedisNotReadySet / 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 不再静默回退 MySQLconfig.DatabaseConfig.DSN() 对非空但未注册的 driver 返回空字符串,database.Dialector 返回初始化即失败的 Dialector;空 driver 仍保持默认 MySQL。下游若使用自定义数据库驱动,需先通过 database.RegisterDialectconfig.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,未配置时回退 expireGenerateTokenWithCustomExpiry 现在拒绝非正过期时间,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 直接返回,避免与 Shutdownwg.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 <= 0window <= 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 条;明确需要全表扫描时改用 FindAllUnboundedFindPage* / QueryBuilder.Page 会归一化 page/pageSize 并限制 MaxPageSize=100MaxPage=10000Find*Ordered / QueryBuilder.Order 只接受简单字段排序(如 created_at DESC, id ASC),复杂表达式/raw SQL 会返回 ErrUnsafeOrderUpdateBatch 字段名不合法返回 ErrUnsafeField

  • 同义可失败 API 收敛:错误不再吞并(cache/jwt/storage/ratelimit):按"同一能力只保留一个主入口、框架不替上层吞错、安全路径默认 fail-closed"原则,收敛历史双轨 API。详见下述分项;自定义 CacheService/Storage 实现需同步更新签名(编译失败即迁移信号)。

    • cacheCacheService.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 */ }

    • jwtTokenBlacklist.IsBlacklisted 改为 (bool, error),删除 IsBlacklistedEIsTokenRevoked 改为 (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)

    • storageStorage.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。新增 ErrReadTooLargeLocalStorage.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) 替代原 NewRedisRateLimiterFailClosedRedisRateLimit / CustomRedisRateLimit / RedisRateLimitWithIdentifier 同步加 opts 参数。删除 NewRedisRateLimiterFailClosed / RedisRateLimitFailClosed / CustomRedisRateLimitFailClosedUploadRedisRateLimit 由 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 包在 InitDBRegisterTLSConfig 注册命名配置(config.MySQLTLSConfigName);CA 不可读或非 PEM 时 fail-fast,不静默回退明文。
  • Postgres 连接支持 sslmode 配置(M-config-2):见 Breaking 项,默认 prefer 优先加密。

Fixed 🐛

  • config Set(cfg) 与 viper 视图同源修复(H-config-1):Set 原只更新类型化视图 m.cfgm.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>0MaxIdleConns>MaxOpenConns 视为配置错误。

  • config 哨兵错误改用 errors.New(L-config-4);DSN() 去除冗余 TrimSpace(L-config-5);DSN/MySQLDSN/PostgresDSN/Addr nil receiver 防御(L-config-6)。

  • database 后台探活/启动 ping 经 pingWithTimeout 3s 约束(H-db-1,M11 修复不完整):pingWithTimeout 原只加到包级 HealthCheck(),未覆盖方法 (*Manager).HealthCheck(被后台探活 probeOnce master 与 /health 端点共用)、probeOnce 从库 ping、InitDB/InitDBWithReplicas 启动 ping。挂起 DB(连接活但不响应)下这些路径的 PingContext 无 ctx deadline 无限阻塞,致探活 goroutine 阻塞、#21 自愈冻结、IsHealthy() 缓存失真、启动卡死。现 4 路径统一经 pingWithTimeouthealthCheckTimeout=3s,尊重 ctx 自带更短 deadline)。另发现并修复 gorm.Open 的 automatic ping(gorm.go:204,ConnPool 为 *sql.DB 时调 pinger.Ping() 无超时)在 pingWithTimeout 之前就无限阻塞--initDB/InitDBWithReplicas 的 gormConfig 加 DisableAutomaticPing: true,框架用 pingWithTimeout 自管启动 ping。Redis 侧 HealthCheck 经 client ReadTimeout(3s) 约束(行为验证)。新增 manager_hdb1_internal_test.go(hungDriver 回归 pingWithTimeout/m.HealthCheck/probeOnce replica/initDB/InitDBWithReplicas 5 路径)、redis_hdb1_internal_test.go(挂起 Redis 回归 HealthCheck)、app_hdb1_test.go(App 级端到端:Init 挂起 DB 有界失败 + Shutdown 正常)。

  • M9 JWT issuer / refresh expiry 契约修复ParseTokenInvalidateTokenGetClaimsFromToken 统一按当前配置校验 issuer;RefreshToken 不再忽略 refresh_expire;空 JTI 不再写入永不命中的 jwt_bl: 黑名单键;新增 ParseTokenFailClosed / ParseTokenWithBlacklistPolicy / TokenBlacklist.IsBlacklistedE,默认 ParseToken 仍保持黑名单检查 fail-open 兼容语义,安全敏感路由可显式选择 fail-closed;解析侧配置错误现在可通过 errors.Is 区分 ErrEmptySecret / ErrUnsupportedAlgorithmInvalidateToken 使用不校验时序的解析路径,允许提前吊销 nbf 在未来的外部 token。

  • M10 分布式锁参数与取消传播修复:锁 TTL 统一校验到 Redis 毫秒粒度;TryLock 的非正 retry interval 不再 busy-loop;WithLockAutoExtend 的非正 extend interval 不再触发 goroutine panic;UnlockByKey 在 Redis 未初始化时与 ForceUnlock 一样返回 ErrRedisNotReadyWithLock / WithLockAutoExtend 现在把调用方 ctx 传入业务函数,避免取消后业务函数继续运行。

  • M10 cache 剩余错误语义收口:新增 cache.GetE / cache.ExistsE 与可选 CacheGetter / CacheExistChecker,让调用方能区分 cache miss、Redis/backend 故障和反序列化错误;保留旧 Get / Exists bool-only 兼容方法但记录后端错误;KeyBuilder 现在忽略 nil option,WithPrefix / WithSeparator / WithCacheType 直接作用于 nil builder 时 no-op,避免扩展配置路径 panic。

  • M2 config 热重载生命周期修复StopWatcher 会等待已触发的 reload/回调结束;包级 Load / LoadWithWatch 只有在新配置成功加载并启动 watcher 后才替换默认 manager,失败时保留旧 watcher;SetDefaultManager 会停止旧 manager 的 watcher,避免全局置换后遗留 goroutine;数据库配置出现字段时会校验 driver/host/name/port,未知 driver 不再静默回退 MySQL,database.Dialector 对未知 driver fail-closed;MySQL DSN 转义用户名/库名,Postgres DSN 统一转义字符串字段。

  • M4 database nil 边界修复InitDB(ctx, nil) / InitDBWithReplicas(ctx, nil, ...) / InitRedis(nil) 现在返回中文错误,不再空指针 panic;UseMaster(nil) / UseReplica(nil) / GetDBFromContext(nil) / WithTx(nil, ...) / TxFromContext(nil) / TransactionWithContext(nil, ...) / ReadQuery(nil, ...) / WriteQuery(nil, ...) / Redis health check 会把 nil context 归一化为 context.Background(),避免异常调用路径触发 panic;Dialector(nil) 安全回退到 MySQL 空 DSN。

  • M4 database 全局置换资源释放修复SetDefaultManager / SetDefaultRedisManager 替换全局默认 manager 时会关闭旧 DB/Redis manager,避免包级默认资源反复置换后连接池泄漏;App 初始化改用 SwapDefaultManager / SwapDefaultRedisManager 暂存旧资源,保证失败回滚仍能恢复旧默认资源。

  • M4 database 初始化生命周期修复:DB 初始化与主从库初始化统一接收 ctx,主库/从库 Ping 使用 PingContext,重试等待改为 select ctx.Done()/time.AfterManager 用生命周期锁串行化 Init/Close,避免 shutdown 与运行期重建交错;运行期重建从库后会立即重建健康标记,探活循环也能在发现健康标记缺失时自愈;包级 HealthCheck() 固定读取一次默认 manager 快照。

  • M11 SSE 换行注入修复WriteEvent 拒绝带 CR/LF 的 event 名,WriteMessage / WriteEvent 的 data 按 SSE 多行格式逐行输出,避免用户数据伪造额外 event:/id: 字段。

  • M15 utils/validation 资源与错误边界修复HTTPClient.Upload 改为流式 multipart 上传,不再把文件请求体完整缓存在内存中;AppendFile / CopyFile 返回写侧 Close 错误;CheckPasswordAndUpgrade 归一化非法 targetCost,避免异常配置触发超高 bcrypt cost;ValidateStruct(nil) 直接返回 nil。

  • M15 utils 转换语义收口:新增 utils.ToIntE / utils.ToInt64E 返回解析错误;旧 ToInt / ToInt64 保留“失败返回 0”的兼容行为,调用方在 0 有业务含义时应迁移到严格变体。

  • M16 测试工具、脚手架与示例闭环修复MockDB / MockCache / MockStorage 改为并发安全;MockCacheMockStorage.UploadFromBytes 复制字节切片,避免调用方修改污染内部状态;MockStorage 拒绝 nil 文件与超过 32MiB 的输入,避免测试 helper 被误用成无上限内存缓冲;xlgo make 对资源名做显式标识符校验,非法名称(路径穿越、连字符、数字开头等)直接返回中文错误,不再静默转义后生成不可预期代码;examples/full 启动时初始化 alice/secret,登录校验 bcrypt 哈希,创建用户也保存哈希,避免示例首次运行无法登录或传播不验密/明文密码模式;README/GUIDE 限流示例不再引用不存在的 handler.Login / handler.Upload

  • M16 GUIDE/test API 不一致修复:GUIDE 测试示例不再调用不存在的 AssertCode / AssertJSONKeyExists,统一改用现有 AssertJSONContains,避免照文档编写测试直接编译失败。

  • M12 storage/compress 安全边界修复:本地上传写侧 Close 错误会通过返回值暴露并清理残片;OSS GetSignedURL 统一经过 object key 净化;UnzipWithOptions 解析目标绝对路径失败时 fail-closed。

  • M8 middleware 边界收口AuthRequiredAuthorization scheme 改为大小写不敏感,符合 Bearer 语义;LoggerConfig.SkipPathPrefixesSimpleLogger 静态路径跳过改为路径边界匹配(/api 仅匹配 /api/api/...,不再误跳过 /api2)。

  • M13 cron handler panic 未 recover 崩进程(cron/cron.go):RunTaskcheckAndRun 调度 goroutine 统一经新增 executeTask(t) 边界 recover,panic 转为 error(含 debug.Stack 调用栈)记入 task.LastError 并向上返回,不再终止进程。外侧 defer wg.Done()/running 守卫释放不受影响(recover 在边界内完成)。顺带修复 RunTask 手动路径此前只更 LastRun/RunCount、不记 LastError 的子问题(现与调度路径一致)。

  • M6 response/handler 入口防护(response/error.go,response/response.go,handler/handler.go):FailWithError(nil) / FailWithDetail(nil, ...) 回退统一服务器错误响应,不再 nil deref panic;新增 response.DownloadReader 支持大文件/对象存储流式下载,旧 Download / DownloadWithContentType 保持兼容并复用同一响应头逻辑。

  • M14 trace 剩余记录补齐(trace/trace.go):SampleRatio 拒绝 NaN/越界值;Init/Close 使用操作超时并在初始化失败时回滚 provider;RecordError / RecordErrorToSpan 对 nil 输入 no-op;Middleware(serviceName) 写入 service.name attribute;X-Trace-ID 不再输出全零 TraceID。

  • M7 health/readiness 探活超时(router/router.go):HealthCheck 新增 Timeout 字段,默认每个依赖检查 2s 超时;超时项返回 "timeout" 并使 /health / /readyz 返回 503。单个 check 同时最多一个执行中,panic 会 recover 为错误,避免 k8s/LB/监控探活被挂死依赖无限卡住或无限堆积 goroutine。

  • M13 cron 剩余边界收口(cron/cron.go):Stop 后再次 Start 会重建调度器 context,手动和调度执行不再收到已取消 ctx;Start/Stop 生命周期串行化,避免 Stop 等待期间重新 Start 触发 WaitGroup Add/Wait 交错。

  • M5 repository 安全边界收口(repository/repository.go):nil ctx 统一按 context.Background() 处理;FindByIDs(nil) 返回非 nil 空切片;NewQueryBuilder 复用 nil DB 明确 panic;批量空 ids 写操作 no-op;默认 FindAll 加上限并新增 FindAllUnbounded;排序/字段名白名单避免便捷 API 误接 raw SQL。

  • M1 App 生命周期三类缺陷统一治理(app.go):

    • Init 失败无资源回滚 → 新增 failAfterInit:markStopping → cancel rootCtx → wg.Wait(10s) → cron 5s 显式超时停止 → closeResources 幂等关闭 db/redis/logger,回滚错误 errors.JoininitErr 不吞;先停 goroutine 再关资源,避免"关 DB 时探活 goroutine 仍在用"的竞态。
    • App.Go 与 wg.Wait racelifecycleMu.RLock 包住 wg.AddShutdown 持写锁翻 stateStopping,保证 Add happens-before Wait。
    • Shutdown 非幂等/非并发安全shutdownOnce 保证 doShutdown 单次执行,并发调用者返回同一 shutdownErr
    • OnStop 语义与超时 → 仅 Init 曾成功(wasInitialized)时在 doShutdown 开头执行;Init 失败/未 Init 时 HTTP 从未启动,OnStop 跳过。OnStop 现在受 server.shutdown_timeout 同一预算约束,阻塞 hook 不再无限拖住 Shutdown。
    • 资源所有权 → App 只关闭自己成功初始化过的 logger/db/redis/cron,避免一个 Init 失败的新 App 关闭同进程既有全局资源。
    • 复审补强:资源替换事务边界 → App 初始化 logger/db/redis 时先创建 App-owned manager 并保存旧默认 manager 快照;OnInit 或后续步骤失败时恢复旧默认 manager 并关闭新资源,完整成功后才释放旧资源,避免“新 App Init 失败”破坏同进程既有全局 logger/db/redis。
    • 复审补强:health/probing 绑定 App-owned manager → App 注册的 MySQL/Redis health check 与 DB probing 使用本 App 持有的 manager,不随后续全局默认 manager 替换漂移。
    • OnReady 早于真实监听成功StartServer 改为同步 net.Listen 且 TLS 证书装配成功后再启动 Serve 与执行 OnReady;监听/TLS 失败直接返回,不触发 ready 副作用。
    • server.unix_socket 不可用 → 非空 unix_socket 现在走 net.Listen("unix", path) + http.Server.Serve,不再把 socket path 误传给 TCP ListenAndServe
    • Init/ShutdowninitMu 串行化 doInit/doShutdown 长段,杜绝并发改资源。
  • M3 logger 生命周期与全局 manager 并发治理(logger/logger.go):

    • LogManager.Init() 锁外写 m.levelInit 在通过局部配置校验后持 m.mu 完成建目录、构造与发布新 logger,m.level 只在同一临界区更新;包级 Logger/fileWriters 发布另由 globalMu 串行化,避免多个 LogManager 实例用各自实例锁保护同一包级状态。
    • DefaultLogger 裸全局指针 → 保留导出变量兼容旧代码,新增内部 atomic 默认 manager 快照与 GetDefaultLogManager();包级 Init/Close/Sync/SetLevel/GetLevel 全部经 atomic 读取当前 manager。
    • stale manager 关闭当前 logger → 每次发布全局 logger 分配 generation,只有拥有当前 generation 的 LogManager.Close() 才能关闭当前全局 logger/writer。
    • 旧 writer 关闭顺序错误Init 先 atomic 发布新 logger,再关闭旧 lumberjack writer,避免替换窗口内包级读路径拿到指向已关闭 writer 的旧 logger。
    • Close()Init() 生命周期互相覆盖Close 在同一 manager 锁内先快照旧 logger/writer 并发布 Nop,再执行 Sync/Close;关闭错误通过 errors.Join 聚合返回,不再静默吞掉 lumberjack Close 错误。
  • M8 middleware/CSRF 与限流边界治理(middleware/csrf.go,middleware/ratelimit.go):

    • CSRF JSON body 读取无上限 → 从 body 提取 _csrf 时使用 http.MaxBytesReader,默认上限 1MiB(CSRFConfig.MaxBodyBytes 可调),超限返回 HTTP 413,避免 pre-auth OOM;仍使用 ShouldBindBodyWith 保留下游重复读取 body 的能力。
    • CSRF cookie SameSite 配置未真正写入CSRF()DoubleSubmitCookie() 设置 cookie 前显式 SetSameSite,默认 Lax。
    • CSRF 局部配置丢默认 cookie 行为CSRFConfig 归一化补齐 PathSameSiteMaxAgeFormFieldMaxBodyBytes 等默认值,只覆盖单个字段时不再意外丢失默认 cookie 约束。
    • CSRF session cookie 显式配置 → 新增 CSRFConfig.SessionCookie,需要会话 Cookie 时设置为 true;为保持 CSRFWithConfig(CSRFConfig{}) 默认 1 小时语义,MaxAge=0 且未设置 SessionCookie 时仍回退默认值。
    • CSRF TokenLength 负数 panic/退化TokenLength <= 0 统一回退默认长度,避免配置错误导致 make([]byte, negative) panic。
    • CSRF skip path 前缀误跳过CSRFWithSkip([]string{"/api"}) 仅跳过 /api/api/...,不再误跳过 /apix
    • API CSRF token map 只校验时清理过期 tokenGenerateAPIToken 颁发新 token 前同步清理过期项,避免长期只发不验场景内存增长。
    • RateLimit(nil) panic → 改为 fail-closed 返回 HTTP 503 + CodeServiceUnavailable,避免未初始化限流器导致请求路径 nil deref。
    • RedisRateLimitWithIdentifier nil 回调 panicidentifierFunc == nil 时回退 ClientIP(),并保留空字符串回退逻辑。
  • gosec database 告警收口(database/manager.go,database/redis.go):RoundRobinPicker 改为 mutex 保护的有界 int 计数器,消除 G115 整数转换告警;RandomPicker 改用 crypto/rand 选择从库,消除 G404 弱随机源告警,随机源失败时安全回退到首个从库;Redis 初始化 ping 失败时不再静默吞掉 client.Close() 错误。

  • 复审补强:DB/Redis 重建路径资源释放(database/manager.go,database/redis.go):Redis 重复初始化会关闭旧 client;DB 重建/重试路径的旧连接池关闭失败不再静默丢弃,会记录 warning 供排查。

  • gosec Close 错误收口(utils/http.go,compress/compress.go):HTTP multipart 上传循环中的本地文件 Close() 错误不再静默吞掉;gzip/zip 解压输出文件关闭失败会通过 errors.Join 返回,并触发残留目标文件清理。

  • gosec 兼容性/误报标注收口(cmd/xlgo,utils/http.go,utils/crypto.go,utils/file.go,compress/compress.go):为已存在输入校验或明确调用方契约的路径、客户端请求 cookie、非安全用途 checksum、压缩源路径遍历、兼容模式 HTTP 请求添加精确 #nosec 理由;NewSSRFSafeHTTPClient / BlockPrivateNetworks 仍是处理不可信 URL 的推荐入口。

  • 示例参数解析错误处理(examples/full/main.go):示例用户详情接口现在检查 fmt.Sscanf 错误,非法用户 ID 返回失败响应,不再静默使用零值。