6b689f3a1b
- 在 .gitignore 中添加调试日志文件的忽略规则,避免不必要的调试信息被提交 - 修改 application.go 中的 stdout 重定向逻辑,使用 log.CaptureStream 以支持更灵活的日志捕获 - 更新 README 文档,增加对日志重定向功能的说明
306 lines
11 KiB
Markdown
306 lines
11 KiB
Markdown
---
|
||
name: objtoobj 问题修正
|
||
overview: 对 objtoobj.go 中发现的 3 个运行时 panic bug、2 个错误处理 bug、多处性能问题进行修正,同时新增销量方案所需的 ObjToRoundFloat64 函数。
|
||
todos:
|
||
- id: fix-uint8-panics
|
||
content: 修复 ObjToFloat64、ObjToInt64、ObjToStr 三处 uint8 分支的 panic bug
|
||
status: completed
|
||
- id: fix-objtomap-shadow
|
||
content: 修复 ObjToMap default 分支变量遮蔽和赋值逻辑错误
|
||
status: completed
|
||
- id: fix-objtoslice-default
|
||
content: 修复 ObjToSlice default 分支 Marshal 错误被覆盖的问题
|
||
status: completed
|
||
- id: fix-objtobool-error
|
||
content: 修复 ObjToBool 成功转换时仍报错的问题
|
||
status: completed
|
||
- id: fix-objtotime-padding
|
||
content: 修复 ObjToTime 单位数日+时间组合时补零失败的 bug
|
||
status: completed
|
||
- id: fix-objtoceilint64-double
|
||
content: 修复 ObjToCeilInt64 双重 Ceil 和多余 ObjToInt64 调用
|
||
status: completed
|
||
- id: perf-type-switch-idiom
|
||
content: 所有函数改用 switch v := obj.(type) 惯用写法,消除重复类型断言
|
||
status: completed
|
||
- id: perf-objtotime
|
||
content: ObjToTime 中 ObjToStr(tInt) 只调用一次并缓存
|
||
status: completed
|
||
- id: perf-objtoslice-string
|
||
content: ObjToSlice []string 分支类型断言只做一次
|
||
status: completed
|
||
- id: perf-objtostr-types
|
||
content: ObjToStr 补充 float32、bool 等常见类型的直接转换
|
||
status: completed
|
||
- id: perf-objtomaparray-cap
|
||
content: ObjToMapArray 预分配切片容量
|
||
status: completed
|
||
- id: add-roundfloat64
|
||
content: 新增 ObjToRoundFloat64,并在 map.go、obj.go 中新增对应 Get/To 方法
|
||
status: completed
|
||
isProject: false
|
||
---
|
||
|
||
# objtoobj.go 问题审查与修正计划
|
||
|
||
修改文件:[common/objtoobj.go](d:\work\hotimev1.5\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`
|
||
|
||
```208:215:d:\work\hotimev1.5\common\objtoobj.go
|
||
case uint8:
|
||
value, e := strconv.ParseFloat(obj.(string), 64)
|
||
```
|
||
|
||
**修正**:改为 `case []byte`,用 `string(v)` 转换后再 ParseFloat:
|
||
|
||
```go
|
||
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`
|
||
|
||
```279:286:d:\work\hotimev1.5\common\objtoobj.go
|
||
case uint8:
|
||
value, e := StrToInt(obj.(string))
|
||
```
|
||
|
||
**修正**:改为 `case []byte`,用 `string(v)` 转换后再 StrToInt:
|
||
|
||
```go
|
||
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` 应改为正确处理单字节
|
||
|
||
```341:342:d:\work\hotimev1.5\common\objtoobj.go
|
||
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 分支 — 变量遮蔽导致错误丢失
|
||
|
||
```33:44:d:\work\hotimev1.5\common\objtoobj.go
|
||
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 错误被覆盖
|
||
|
||
```86:91:d:\work\hotimev1.5\common\objtoobj.go
|
||
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` — 成功转换时也报错
|
||
|
||
```307:330:d:\work\hotimev1.5\common\objtoobj.go
|
||
default:
|
||
toInt := ObjToInt(obj)
|
||
if toInt != 0 {
|
||
v = true
|
||
}
|
||
err = errors.New("没有合适的转换对象!")
|
||
```
|
||
|
||
当传入 `1`(int 类型)时,`ObjToInt` 成功返回 1,`v = true`,但仍然设置了 `err`。逻辑上这不应该报错。
|
||
|
||
**修正**:只在 `ObjToInt` 也无法转换时才报错。
|
||
|
||
---
|
||
|
||
### Bug 7: `ObjToTime` 日期补零逻辑缺陷
|
||
|
||
```108:121:d:\work\hotimev1.5\common\objtoobj.go
|
||
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
|
||
|
||
```238:241:d:\work\hotimev1.5\common\objtoobj.go
|
||
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 次
|
||
|
||
```152:177:d:\work\hotimev1.5\common\objtoobj.go
|
||
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` 分支重复类型断言
|
||
|
||
```78:81:d:\work\hotimev1.5\common\objtoobj.go
|
||
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:
|
||
|
||
```go
|
||
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 惯用写法可消除所有冗余断言:
|
||
|
||
```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` 类型。
|
||
|
||
---
|
||
|
||
## 四、结合销量方案需新增的函数
|
||
|
||
根据[销量支持小数方案](d:\work\your-app.cursor\plans\销量支持小数方案_92094ec9.plan.md)第二步要求,需在本文件新增 `ObjToRoundFloat64`:
|
||
|
||
```go
|
||
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](d:\work\hotimev1.5\common\map.go) 新增 `GetRoundFloat64`,在 [common/obj.go](d:\work\hotimev1.5\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 | 销量方案需要 | 新增函数 |
|
||
|
||
|