Files
hotime/.cursor/plans/objtoobj_问题修正_a5e6aae7.plan.md
T
hoteas 6b689f3a1b chore(logging): 更新日志重定向与捕获功能
- 在 .gitignore 中添加调试日志文件的忽略规则,避免不必要的调试信息被提交
- 修改 application.go 中的 stdout 重定向逻辑,使用 log.CaptureStream 以支持更灵活的日志捕获
- 更新 README 文档,增加对日志重定向功能的说明
2026-07-13 07:45:51 +08:00

11 KiB
Raw Blame History

name, overview, todos, isProject
name overview todos isProject
objtoobj 问题修正 对 objtoobj.go 中发现的 3 个运行时 panic bug、2 个错误处理 bug、多处性能问题进行修正,同时新增销量方案所需的 ObjToRoundFloat64 函数。
id content status
fix-uint8-panics 修复 ObjToFloat64、ObjToInt64、ObjToStr 三处 uint8 分支的 panic bug completed
id content status
fix-objtomap-shadow 修复 ObjToMap default 分支变量遮蔽和赋值逻辑错误 completed
id content status
fix-objtoslice-default 修复 ObjToSlice default 分支 Marshal 错误被覆盖的问题 completed
id content status
fix-objtobool-error 修复 ObjToBool 成功转换时仍报错的问题 completed
id content status
fix-objtotime-padding 修复 ObjToTime 单位数日+时间组合时补零失败的 bug completed
id content status
fix-objtoceilint64-double 修复 ObjToCeilInt64 双重 Ceil 和多余 ObjToInt64 调用 completed
id content status
perf-type-switch-idiom 所有函数改用 switch v := obj.(type) 惯用写法,消除重复类型断言 completed
id content status
perf-objtotime ObjToTime 中 ObjToStr(tInt) 只调用一次并缓存 completed
id content status
perf-objtoslice-string ObjToSlice []string 分支类型断言只做一次 completed
id content status
perf-objtostr-types ObjToStr 补充 float32、bool 等常见类型的直接转换 completed
id content status
perf-objtomaparray-cap ObjToMapArray 预分配切片容量 completed
id content status
add-roundfloat64 新增 ObjToRoundFloat64,并在 map.go、obj.go 中新增对应 Get/To 方法 completed
false

objtoobj.go 问题审查与修正计划

修改文件:common/objtoobj.go


一、严重 Buguint8 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: ObjToFloat64case 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: ObjToInt64case 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: ObjToStrcase 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("没有合适的转换对象!")

当传入 1int 类型)时,ObjToInt 成功返回 1v = 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 缺少 float32bool 等常见类型直接转换

float32boolint32 等类型会走 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

MarshalIndentMarshal 慢 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 遮蔽内置类型

参数名应改为 sjsonStr,避免遮蔽 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 销量方案需要 新增函数