ffcb1a77b9
对 60 个非测试源文件做文档/注释/代码一致性审查,修复 23 项。全部为注释/文档 改动 + 1 处死代码删除,无运行逻辑变更,无 breaking change。 A 档·实质错误(10 项,会误导下游): - app.go: closeResources 三连契约注释统一修正--failAfterInit 走 rollbackReplacedResources 不走 closeResources,closeResources 仅供 doShutdown 复用 - database/manager.go: RandomPicker 注释从过时的 rand.Intn/issue 54899 改为 crypto/rand - model/base.go: datetime(0) 与“亚秒精度”矛盾,改为 datetime(6) - cache/keybuilder.go: 三处示例下划线分隔符改为冒号(与默认分隔符 : 一致) - repository/repository.go: Delete 接口注释从绝对“软删除”改为条件性; UpdateFields 示例移除把字段名当 WHERE 条件的误用写法 - middleware/csrf.go: CSRFToken 注释从“用于 API 模式”改为 Cookie/双重提交模式说明 - examples/full/main.go: POST /users 补上“需 Authorization”标注;删除 _ = app 死代码 - utils/random.go: RandInt/RandInt64 补 min==max 边界说明(对齐 RandIntSecure 写法) B 档·导出符号注释缺失/不完整(6 项): - ws/ws.go: 5 个 Type* 常量、4 个 Err* 变量补注释 - console/console.go: 5 个 Level 常量补注释 - jwt/jwt.go: 2 个 Blacklist 常量补注释 - router/router.go: 包级 GroupWithMiddlewareGroup 补注释 - middleware/ratelimit.go: NewRateLimiter 补 panic 条件与 Stop 生命周期说明 C 档·描述不完整(7 项): - utils/strings.go Substr、convert.go CalcPageCount/CalcOffset、datetime.go StartOfMonth/EndOfMonth、file.go CopyFile、validator.go IsChinese、crypto.go HashFile 补全边界/默认行为/返回值语义 - response/error.go: WithDetail 补 nil 接收者行为说明 验证:go build / go vet / go test -race 全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
293 lines
6.9 KiB
Go
293 lines
6.9 KiB
Go
package console
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"runtime"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
)
|
||
|
||
// writeMu 串行化所有 Console 的写出序列(P1 #7)。
|
||
// 用包级锁而非 per-Console 锁:多个 Console 实例可能共享同一底层 writer(如 stdout),
|
||
// per-Console 锁无法阻止跨实例交错;且 Windows printColor 的"设色→写→复位"三步必须整体
|
||
// 原子,否则并发彩色输出会串色。控制台输出非热路径,单一全局写锁的串行化开销可忽略。
|
||
var writeMu sync.Mutex
|
||
|
||
// Level 日志级别
|
||
type Level int32
|
||
|
||
const (
|
||
// LevelDebug 调试级别(最低)。
|
||
LevelDebug Level = iota
|
||
// LevelInfo 普通信息。
|
||
LevelInfo
|
||
// LevelSuccess 成功信息。
|
||
LevelSuccess
|
||
// LevelWarn 警告。
|
||
LevelWarn
|
||
// LevelError 错误(最高常规级别)。
|
||
LevelError
|
||
|
||
// LevelSilent 完全静默:所有调用都不输出
|
||
LevelSilent Level = 127
|
||
)
|
||
|
||
// String 返回级别名称
|
||
func (l Level) String() string {
|
||
if c, ok := colors[l]; ok {
|
||
return c.Name
|
||
}
|
||
if l == LevelSilent {
|
||
return "Silent"
|
||
}
|
||
return "Unknown"
|
||
}
|
||
|
||
// Color 颜色定义
|
||
type Color struct {
|
||
Code string
|
||
Name string
|
||
}
|
||
|
||
var colors = map[Level]Color{
|
||
LevelDebug: {Code: "0;36", Name: "Debug"}, // 青色
|
||
LevelInfo: {Code: "0;37", Name: "Info"}, // 白色
|
||
LevelSuccess: {Code: "0;92", Name: "Success"}, // 亮绿色
|
||
LevelWarn: {Code: "1;93", Name: "Warn"}, // 亮黄色
|
||
LevelError: {Code: "1;31", Name: "Error"}, // 亮红色
|
||
}
|
||
|
||
// Console 控制台打印器。
|
||
//
|
||
// console 包定位:开发期彩色 stdout 工具,跟 fmt.Println 同级。
|
||
// 不写文件、不感知运行环境、不做任何隐式 level 切换——
|
||
// 所有 level 行为都由调用方显式控制(SetLevel / WithLevel)。
|
||
//
|
||
// 业务可观测信息(用户登录、订单状态变更等"上线后必须保留的事件")
|
||
// 请使用 logger 包;console 仅用于开发期肉眼调试。
|
||
type Console struct {
|
||
output io.Writer
|
||
isColor bool
|
||
showTime bool
|
||
showCall bool
|
||
timeFmt string
|
||
skipCall int
|
||
|
||
// level 通过 atomic 访问,支持运行期热切换且并发安全。
|
||
// 用 int32 存储 Level,0 = LevelDebug,与零值默认对齐。
|
||
level atomic.Int32
|
||
}
|
||
|
||
// Option 配置选项
|
||
type Option func(*Console)
|
||
|
||
// WithOutput 设置输出目标
|
||
func WithOutput(w io.Writer) Option {
|
||
return func(c *Console) {
|
||
c.output = w
|
||
}
|
||
}
|
||
|
||
// WithColor 设置是否启用颜色
|
||
func WithColor(enable bool) Option {
|
||
return func(c *Console) {
|
||
c.isColor = enable
|
||
}
|
||
}
|
||
|
||
// WithTime 设置是否显示时间
|
||
func WithTime(show bool) Option {
|
||
return func(c *Console) {
|
||
c.showTime = show
|
||
}
|
||
}
|
||
|
||
// WithCaller 设置是否显示调用位置。
|
||
// skip 可选,默认 2(直接调用方);自封装一层时传 3。
|
||
func WithCaller(show bool, skip ...int) Option {
|
||
return func(c *Console) {
|
||
c.showCall = show
|
||
if len(skip) > 0 && skip[0] > 0 {
|
||
c.skipCall = skip[0]
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithTimeFormat 设置时间格式
|
||
func WithTimeFormat(fmt string) Option {
|
||
return func(c *Console) {
|
||
c.timeFmt = fmt
|
||
}
|
||
}
|
||
|
||
// WithLevel 设置最低输出级别。低于该级别的调用会被静默丢弃。
|
||
//
|
||
// 例:WithLevel(LevelWarn) 只输出 Warn 与 Error;
|
||
//
|
||
// WithLevel(LevelSilent) 完全静默,常用于压测或上线观察期临时关闭调试输出。
|
||
func WithLevel(l Level) Option {
|
||
return func(c *Console) {
|
||
c.level.Store(int32(l))
|
||
}
|
||
}
|
||
|
||
// New 创建控制台打印器
|
||
func New(opts ...Option) *Console {
|
||
c := &Console{
|
||
output: os.Stdout,
|
||
isColor: true,
|
||
showTime: true,
|
||
showCall: true,
|
||
timeFmt: "15:04:05.000",
|
||
skipCall: 2,
|
||
}
|
||
// 默认 LevelDebug:开发期所有级别都打印。生产期请显式 SetLevel/WithLevel。
|
||
c.level.Store(int32(LevelDebug))
|
||
for _, opt := range opts {
|
||
opt(c)
|
||
}
|
||
return c
|
||
}
|
||
|
||
// SetLevel 运行期切换最低输出级别。并发安全。
|
||
func (c *Console) SetLevel(l Level) {
|
||
c.level.Store(int32(l))
|
||
}
|
||
|
||
// Level 返回当前最低输出级别
|
||
func (c *Console) Level() Level {
|
||
return Level(c.level.Load())
|
||
}
|
||
|
||
// Default 默认控制台
|
||
var Default = New()
|
||
|
||
// SetLevel 设置默认控制台的最低输出级别。并发安全。
|
||
//
|
||
// 典型用法(在 main 中根据 cfg 显式切换):
|
||
//
|
||
// if cfg.IsProduction() {
|
||
// console.SetLevel(console.LevelWarn) // 生产期只看 Warn / Error
|
||
// }
|
||
//
|
||
// 框架不会自动根据环境模式切换,选择权完全在调用方。
|
||
func SetLevel(l Level) {
|
||
Default.SetLevel(l)
|
||
}
|
||
|
||
// GetLevel 返回默认控制台当前最低输出级别。
|
||
// (命名加 Get 前缀是因为 Level 已被类型占用,Go 不允许同名函数。)
|
||
func GetLevel() Level {
|
||
return Default.Level()
|
||
}
|
||
|
||
// print 内部打印函数
|
||
func (c *Console) print(level Level, s ...any) {
|
||
// 级别过滤:低于阈值或调用方显式 LevelSilent 时直接返回,零开销
|
||
if level < c.Level() {
|
||
return
|
||
}
|
||
|
||
var sb strings.Builder
|
||
|
||
// 时间
|
||
if c.showTime {
|
||
sb.WriteString(time.Now().Format(c.timeFmt))
|
||
sb.WriteString(" ")
|
||
}
|
||
|
||
// 级别名称
|
||
color := colors[level]
|
||
sb.WriteString("[")
|
||
sb.WriteString(color.Name)
|
||
sb.WriteString("] ")
|
||
|
||
// 调用位置
|
||
if c.showCall {
|
||
sb.WriteString(c.getCaller())
|
||
sb.WriteString(" ")
|
||
}
|
||
|
||
// 内容
|
||
sb.WriteString(fmt.Sprint(s...))
|
||
|
||
// 输出。P1 #7:全局写锁串行化整个写序列,避免并发交错输出;
|
||
// Windows 下 printColor 的"设色→写→复位"三步也被此锁整体保护,防止串色。
|
||
writeMu.Lock()
|
||
if c.isColor {
|
||
c.printColor(color.Code, sb.String())
|
||
} else {
|
||
fmt.Fprintln(c.output, sb.String())
|
||
}
|
||
writeMu.Unlock()
|
||
}
|
||
|
||
// getCaller 获取调用位置
|
||
func (c *Console) getCaller() string {
|
||
_, file, line, ok := runtime.Caller(c.skipCall)
|
||
if !ok {
|
||
return ""
|
||
}
|
||
// 只取文件名(兼容 / 与 \ 分隔)
|
||
if idx := strings.LastIndexAny(file, "/\\"); idx >= 0 {
|
||
file = file[idx+1:]
|
||
}
|
||
return fmt.Sprintf("%s:%d", file, line)
|
||
}
|
||
|
||
// Debug 打印调试信息(青色)
|
||
func (c *Console) Debug(s ...any) {
|
||
c.print(LevelDebug, s...)
|
||
}
|
||
|
||
// Info 打印普通信息(白色)
|
||
func (c *Console) Info(s ...any) {
|
||
c.print(LevelInfo, s...)
|
||
}
|
||
|
||
// Success 打印成功信息(绿色)
|
||
func (c *Console) Success(s ...any) {
|
||
c.print(LevelSuccess, s...)
|
||
}
|
||
|
||
// Warn 打印警告信息(黄色)
|
||
func (c *Console) Warn(s ...any) {
|
||
c.print(LevelWarn, s...)
|
||
}
|
||
|
||
// Error 打印错误信息(红色)
|
||
func (c *Console) Error(s ...any) {
|
||
c.print(LevelError, s...)
|
||
}
|
||
|
||
// ===== 包级别便捷函数 =====
|
||
|
||
// Debug 使用默认控制台打印调试信息
|
||
func Debug(s ...any) {
|
||
Default.print(LevelDebug, s...)
|
||
}
|
||
|
||
// Info 使用默认控制台打印普通信息
|
||
func Info(s ...any) {
|
||
Default.print(LevelInfo, s...)
|
||
}
|
||
|
||
// Success 使用默认控制台打印成功信息
|
||
func Success(s ...any) {
|
||
Default.print(LevelSuccess, s...)
|
||
}
|
||
|
||
// Warn 使用默认控制台打印警告信息
|
||
func Warn(s ...any) {
|
||
Default.print(LevelWarn, s...)
|
||
}
|
||
|
||
// Error 使用默认控制台打印错误信息
|
||
func Error(s ...any) {
|
||
Default.print(LevelError, s...)
|
||
}
|