- 在 .gitignore 中添加调试日志文件的忽略规则,避免不必要的调试信息被提交 - 修改 application.go 中的 stdout 重定向逻辑,使用 log.CaptureStream 以支持更灵活的日志捕获 - 更新 README 文档,增加对日志重定向功能的说明
11 KiB
name, overview, todos, isProject
| name | overview | todos | isProject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| objtoobj 问题修正 | 对 objtoobj.go 中发现的 3 个运行时 panic bug、2 个错误处理 bug、多处性能问题进行修正,同时新增销量方案所需的 ObjToRoundFloat64 函数。 |
|
false |
objtoobj.go 问题审查与修正计划
修改文件:common/objtoobj.go
一、严重 Bug(uint8 vs []byte 类型混淆,可能 panic)
开发者原意是处理数据库 driver 返回的 []uint8(即 []byte),但 Go 中 uint8(单字节)和 []uint8/[]byte(字节数组)是完全不同的类型,type switch 不会互相匹配。
当前 Row() 方法(query.go:380)已经用 reflect 将 []uint8 转为 string,所以正常 DB 流程不会触发这些分支。但其他非 DB 数据源可能传入 []byte,此时会走 default 分支而非预期的 uint8 分支。
Bug 1: ObjToFloat64 — case uint8 应为 case []byte
case uint8:
value, e := strconv.ParseFloat(obj.(string), 64)
修正:改为 case []byte,用 string(v) 转换后再 ParseFloat:
case []byte:
value, e := strconv.ParseFloat(string(obj.([]byte)), 64)
if e != nil {
v = float64(0)
err = e
} else {
v = value
}
Bug 2: ObjToInt64 — case uint8 应为 case []byte
case uint8:
value, e := StrToInt(obj.(string))
修正:改为 case []byte,用 string(v) 转换后再 StrToInt:
case []byte:
value, e := StrToInt(string(obj.([]byte)))
if e != nil {
v = int64(0)
err = e
} else {
v = int64(value)
}
Bug 3: ObjToStr — case uint8 应改为正确处理单字节
case uint8:
str = obj.(string)
ObjToStr 已有 case []byte 分支正确处理字节数组。这里的 case uint8 应正确处理单字节值:
修正:改为 str = strconv.Itoa(int(obj.(uint8)))
这三处 bug 目前未爆发,因为
Row()已将[]uint8转为string。但修正后能覆盖非 DB 数据源直接传入[]byte的场景,更加健壮。
二、逻辑 Bug
Bug 4: ObjToMap default 分支 — 变量遮蔽导致错误丢失
default:
data, err := json.Marshal(obj)
if err != nil {
err = errors.New("没有合适的转换对象!" + err.Error())
v = nil
}
v = Map{}
e := json.Unmarshal(data, &v)
第 34 行的 data, err := json.Marshal(obj) 使用了 :=,创建了一个新的局部变量 err,遮蔽了第 14 行声明的外部 err。这意味着:
- Marshal 失败时,外部
err仍然是 nil,错误信息丢失 - 第 38 行设
v = nil,但第 39 行又立刻把v重新赋值为Map{},然后用坏数据尝试 Unmarshal
修正:用 = 代替 :=,并在 Marshal 失败时 break。
Bug 5: ObjToSlice default 分支 — Marshal 错误被覆盖
default:
v = Slice{}
var data []byte
data, err = json.Marshal(obj)
err = json.Unmarshal(data, &v)
第 89 行 err = json.Marshal(obj) 即使失败,第 90 行也立刻用 err = json.Unmarshal(data, &v) 覆盖了前一个错误。当 Marshal 失败时 data 为空,Unmarshal 也会失败,但丢失了原始的错误信息。
修正:Marshal 失败时直接 break,不再尝试 Unmarshal。
Bug 6: ObjToBool — 成功转换时也报错
default:
toInt := ObjToInt(obj)
if toInt != 0 {
v = true
}
err = errors.New("没有合适的转换对象!")
当传入 1(int 类型)时,ObjToInt 成功返回 1,v = true,但仍然设置了 err。逻辑上这不应该报错。
修正:只在 ObjToInt 也无法转换时才报错。
Bug 7: ObjToTime 日期补零逻辑缺陷
timeNewStrs := strings.Split(tStr, "-")
for _, v := range timeNewStrs {
if len(v) == 1 {
v = "0" + v
}
}
按 "-" 分割 "2006-1-2 15:04:05" 得到 ["2006", "1", "2 15:04:05"]。第三段 "2 15:04:05" 长度 > 1 不会补零,最终得到 "2006-01-2 15:04:05",time.Parse 会失败。
修正:分割前先按空格分离日期和时间部分,分别处理后再拼接。或者改用 strings.TrimLeft 对每段的纯日期部分补零。
Bug 8: ObjToCeilInt64 双重 Ceil + 多余 ObjToInt64
func ObjToCeilInt64(obj interface{}, e ...*Error) int64 {
f := ObjToCeilFloat64(obj, e...) // 内部已 math.Ceil
return ObjToInt64(math.Ceil(f)) // 又 Ceil + 走完整 type switch
}
修正:改为 return int64(f),因为 ObjToCeilFloat64 已经返回了 Ceil 后的值。
三、性能问题
P1: ObjToTime 重复调用 ObjToStr(tInt) 多达 5 次
if len(ObjToStr(tInt)) > 16 {
// ...
} else if len(ObjToStr(tInt)) > 13 {
// ...
} else if len(ObjToStr(tInt)) > 10 {
// ...
} else if len(ObjToStr(tInt)) > 9 {
// ...
} else if len(ObjToStr(tInt)) > 3 {
t, e := time.Parse("2006", ObjToStr(tInt))
每次 ObjToStr(tInt) 都会执行 strconv.FormatInt + 分配新字符串。应在循环外只调用一次,存入变量复用。
P2: ObjToSlice 的 []string 分支重复类型断言
case []string:
v = Slice{}
for i := 0; i < len(obj.([]string)); i++ {
v = append(v, obj.([]string)[i])
}
循环中每次 obj.([]string) 都做了类型断言。应先断言一次存入变量:ss := obj.([]string)。
P3: ObjToStr 缺少 float32、bool 等常见类型直接转换
float32、bool、int32 等类型会走 default 分支用 json.MarshalIndent,比直接转换慢 100 倍以上。
修正:补充常见类型的 case:
case float32:
str = strconv.FormatFloat(float64(obj.(float32)), 'f', -1, 32)
case bool:
str = strconv.FormatBool(obj.(bool))
P4: ObjToStr default 分支使用 json.MarshalIndent
MarshalIndent 比 Marshal 慢 20-30%,且产生更大的输出。但由于此改动可能影响现有依赖格式化输出的调用方,标记为可选优化。
P5: 所有函数应改用 switch v := obj.(type) 惯用写法
当前每个 case 都重复做类型断言(如 obj.(int)),改为 Go 惯用写法可消除所有冗余断言:
// 改前
switch obj.(type) {
case int:
v = int64(obj.(int))
// 改后
switch val := obj.(type) {
case int:
v = int64(val)
P6: ObjToMapArray 未预分配切片容量
res := []Map{} 应改为 res := make([]Map, 0, len(s))。
P7: StrToMap / StrToSlice 参数名 string 遮蔽内置类型
参数名应改为 s 或 jsonStr,避免遮蔽 Go 内置 string 类型。
四、结合销量方案需新增的函数
根据销量支持小数方案第二步要求,需在本文件新增 ObjToRoundFloat64:
func ObjToRoundFloat64(obj interface{}, precision int, e ...*Error) float64 {
f := ObjToFloat64(obj, e...)
if precision < 0 {
return f
}
pow := math.Pow10(precision)
return math.Round(f*pow) / pow
}
同时需在 common/map.go 新增 GetRoundFloat64,在 common/obj.go 新增 ToRoundFloat64。
五、修正摘要
| # | 类型 | 位置 | 问题 | 修正 |
|---|---|---|---|---|
| 1 | 类型混淆 | ObjToFloat64 uint8 | case uint8 应为 case []byte | 改为 case []byte + string() 解析 |
| 2 | 类型混淆 | ObjToInt64 uint8 | case uint8 应为 case []byte | 改为 case []byte + string() 解析 |
| 3 | 类型混淆 | ObjToStr uint8 | uint8 断言为 string 会 panic | 改为 strconv.Itoa(int(...)) |
| 4 | 逻辑 | ObjToMap default | := 遮蔽外部 err,v 赋值逻辑错乱 | 用 = 并加 break |
| 5 | 逻辑 | ObjToSlice default | Marshal 错误被 Unmarshal 覆盖 | 失败时 break |
| 6 | 逻辑 | ObjToBool default | 成功转换也报错 | 转换成功不报错 |
| 7 | 逻辑 | ObjToTime 日期补零 | 单位数日+时间时补不到零 | 分离日期时间部分再补零 |
| 8 | 逻辑 | ObjToCeilInt64 | 双重 Ceil + 多余 type switch | 改为 int64(f) |
| 9 | 性能 | ObjToTime | ObjToStr 重复调用 5 次 | 缓存结果 |
| 10 | 性能 | ObjToSlice []string | 循环内重复类型断言 | 断言一次复用 |
| 11 | 性能 | 所有函数 | 未用 switch v := obj.(type) | 改用惯用写法消除冗余断言 |
| 12 | 性能 | ObjToStr | 缺少 float32/bool 等常见类型 | 补充直接转换 case |
| 13 | 性能 | ObjToMapArray | 切片未预分配容量 | make([]Map, 0, len(s)) |
| 14 | 规范 | StrToMap/StrToSlice | 参数名 string 遮蔽内置类型 | 改为 s 或 jsonStr |
| 15 | 新增 | ObjToRoundFloat64 | 销量方案需要 | 新增函数 |