--- 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 | 销量方案需要 | 新增函数 |