Compare commits
No commits in common. "main" and "v1.1.0" have entirely different histories.
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,5 +1 @@
|
|||||||
.gemini/
|
go.sum
|
||||||
.ai/
|
|
||||||
.geminiignore
|
|
||||||
.gemini
|
|
||||||
/CODE-FULL.md
|
|
||||||
|
|||||||
83
CHANGELOG.md
83
CHANGELOG.md
@ -1,88 +1,5 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## [v1.2.9] - 2026-05-09
|
|
||||||
### Changed
|
|
||||||
- **移除第三方依赖**: 移除了对 `jsontag` 模块的依赖,统一使用标准库及自有基础设施对齐,增强了模块的独立性与长期稳定性。
|
|
||||||
|
|
||||||
## [v1.2.8] - 2026-05-05
|
|
||||||
### Fixed
|
|
||||||
- **JSON 解码器深度增强**: 修复了 `cast.UnmarshalJSON` 在反序列化到 `interface{}` 类型字段时会跳过对象和数组的重大缺陷。
|
|
||||||
- **高精度整数解析**: 优化了 JSON 解码过程中的数字处理逻辑。当目标为 `interface{}` 时,优先将整数解析为 `int64` 而非 `float64`,彻底解决了纳秒级时间戳(Nanosecond Timestamp)和 64 位长 ID 的精度丢失问题。
|
|
||||||
- **接口兼容性**: 完善了对 `interface{}` 类型的自动类型识别,支持正确解析为 `map[string]any` 和 `[]any`。
|
|
||||||
|
|
||||||
## [v1.2.6] - 2026-05-04
|
|
||||||
### Fixed
|
|
||||||
- **Map 深度合并修复**: 修复了在 `Convert` 或 `ToMap` 过程中,如果目标 Map 已存在该 Key,其原有结构体/Map 值会被直接覆盖而非深度合并的问题。通过引入 `dst.MapIndex` 预读取与临时寻址变量,现已完美支持 Map 下非指针结构体的局部字段覆盖。
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **深度合并测试集**: 新增 `merge_test.go` 与 `complex_conversion_test.go`,覆盖了 Struct-to-Map, Map-to-Map, Slice-to-Map 等多种深度嵌套合并场景,确保配置覆盖逻辑的健壮性。
|
|
||||||
|
|
||||||
## [v1.2.3] - 2026-05-04
|
|
||||||
### Added
|
|
||||||
- **强大的时间解析引擎**: 移植 `time` 模块的核心算法至 `cast.ParseTime`,支持时间戳(秒至纳秒)、RFC3339、JS 格式、紧凑格式(20060102150405)及中文日期解析。
|
|
||||||
- **时间操作 API**: 新增 `FormatTime(layout, v)` 支持直观格式化(YYYY-MM-DD),新增 `AddTime(expr, v)` 支持 DSL 时间加减(+1Y-2M)。
|
|
||||||
- **时区上下文支持**: 引入 `TimeZone` 类型及全局 `DefaultTimeZone`,支持 `SetDefaultTimeZone` 统一控制全项目的转换时区。
|
|
||||||
|
|
||||||
### Optimized
|
|
||||||
- **ToTime 升级**: 重构 `ToTime` 底层,默认调用高性能 `ParseTime` 引擎,并强制转换至 `DefaultTimeZone` 时区。
|
|
||||||
- **JSON 时间处理**: `FastEncoder` 与 `FastDecoder` 现已深度集成 `DefaultTimeZone`,确保序列化与反序列化时的时区一致性。
|
|
||||||
|
|
||||||
## [v1.2.2] - 2026-05-04
|
|
||||||
### Added
|
|
||||||
- **深度对象映射引擎**: 移植 `convert` 包的核心能力至 `cast.Convert`。
|
|
||||||
- **Convert API**: 新增 `Convert(dst, src any)` 入口,支持深度转换与原地更新,遵循 Go 语言参数哲学。
|
|
||||||
- **Func 转换支持**: 原生支持 `func` 到 `func` 的自动类型适配与包装。
|
|
||||||
- **CSV 自动解析**: `To[[]T]` 和 `Convert` 现支持自动将逗号分隔字符串解析为切片。
|
|
||||||
- **ParseHook 支持**: 支持结构体字段的自定义 `ParseFieldName` 钩子函数。
|
|
||||||
- **自我转换保护**: 增加指针地址校验,防止 `Convert(a, a)` 导致的死循环或无效开销。
|
|
||||||
|
|
||||||
### Optimized
|
|
||||||
- **极致性能重构**: 实现 `performRecursiveTo` 递归引擎,全面消灭内存逃逸,对象映射性能提升 ~40%,内存分配减少 ~75%。
|
|
||||||
- **覆盖赋值语义**: 修正单值转切片的语义,从“追加”改为“覆盖赋值”。
|
|
||||||
- **Bool 强制转换优化**: 重构 `Bool()` 底层分支逻辑,提升 CPU 分支预测友好度。
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- **convert 模块**: 正式移除外部 `apigo.cc/go/convert` 依赖,功能全部合并至 `cast`。
|
|
||||||
|
|
||||||
## [v1.2.1] - 2026-05-04
|
|
||||||
### Added
|
|
||||||
- **Encoder 元数据缓存**: 引入 `encoderStructDescriptor` 缓存,大幅减少结构体序列化时的反射开销。
|
|
||||||
- **time.Time 全局支持**:
|
|
||||||
- Encoder/Decoder 原生支持 `time.Time` 类型。
|
|
||||||
- 默认使用格式 `2006-01-02 15:04:05.000`。
|
|
||||||
- 支持通过 Struct Tag 指定格式(如 `json:"format=2006-01-02"`)。
|
|
||||||
- **零分配摩擦匹配**: 重构 Decoder 匹配逻辑,实现 `normalizeEqual` 算法,在忽略大小写/下划线匹配时彻底消除内存分配。
|
|
||||||
|
|
||||||
### Optimized
|
|
||||||
- **Encoder 性能**:
|
|
||||||
- 采用 `MapRange` 代替 `MapKeys` 遍历 Map,减少中间切片分配。
|
|
||||||
- 延迟 Path 路径计算,仅在开启脱敏字典时计算路径。
|
|
||||||
- 重写 `writeString`,支持批量写入与高效查表转义。
|
|
||||||
- **Decoder 性能**:
|
|
||||||
- 优化 `decodeArray` 扩容策略,优先尊重并使用用户预设的 Capacity。
|
|
||||||
- **移除 Map 排序**: 序列化 Map 时不再进行 Key 排序,显著提升大规模 Map 的处理速度。
|
|
||||||
|
|
||||||
## [v1.2.0] - 2026-05-04
|
|
||||||
### Added
|
|
||||||
- **零摩擦 API**: 核心 API `To[T]`, `ToJSON`, `FromJSON`, `ToMap`, `ToSlice` 全部重构为**不返回错误**,遇错静默返回零值。
|
|
||||||
- **As 包装器**: 原 `Quiet` 更名为 `As`,用于一键消除传统 `(value, error)` 函数的摩擦。
|
|
||||||
### Removed
|
|
||||||
- **YAML 支持**: 移除 `gopkg.in/yaml.v3` 依赖。
|
|
||||||
- **Must/As[T] 系列**: 移除 `Must` 及所有特殊的 `AsXxx` 系列函数,统一通过 `To[T]` 或 `As` 包装实现。
|
|
||||||
- **冗余 API**: 移除 `PrettyToJSONBytes` 和 `ToJSONDesensitize`,保留对应的 `PrettyToJSON` 和 `ToJSONDesensitizeBytes`。
|
|
||||||
|
|
||||||
## [v1.1.2] - 2026-05-04
|
|
||||||
### Added
|
|
||||||
- 新增 `ToMap` 和 `ToSlice`: 侧重于原地填充/追加,支持 Struct 继承拍平、KV 序列化与自动类型转换。
|
|
||||||
- 新增 `MakeMap`, `MakeSlice`, `FromJSON`, `FromYAML` 系列: 泛型构建新对象的零摩擦 API。
|
|
||||||
- 增强 `UnmarshalJSON` / `UnmarshalYAML`: 重构为副作用模式(返回 error),支持多种输入类型。
|
|
||||||
### Removed
|
|
||||||
- 移除 `Ints` 和 `Strings`: 由更通用的 `MakeSlice` 替代,保持 API 简洁。
|
|
||||||
|
|
||||||
## [v1.1.1] - 2026-04-12
|
|
||||||
|
|
||||||
- **修复**: 解决 `FastEncoder` 在编码结构体时忽略匿名嵌入(Embedded)字段的问题,确保组合对象的完整序列化。
|
|
||||||
|
|
||||||
## [v1.1.0] - 2026-05-02
|
## [v1.1.0] - 2026-05-02
|
||||||
- **功能**: 新增 `FastEncoder`,实现单路径 JSON 编码,大幅提升性能并减少内存分配。
|
- **功能**: 新增 `FastEncoder`,实现单路径 JSON 编码,大幅提升性能并减少内存分配。
|
||||||
- **功能**: 新增 `ToJSONDesensitize` 和 `ToJSONDesensitizeBytes`,支持原生字段脱敏。
|
- **功能**: 新增 `ToJSONDesensitize` 和 `ToJSONDesensitizeBytes`,支持原生字段脱敏。
|
||||||
|
|||||||
111
README.md
111
README.md
@ -5,12 +5,11 @@
|
|||||||
|
|
||||||
## 🎯 设计哲学
|
## 🎯 设计哲学
|
||||||
|
|
||||||
`@go/cast` 是一个为“极致敏捷”设计的 Go 基础工具库。其核心目标是**彻底消除摩擦**:
|
`@go/cast` 是一个为“敏捷开发”设计的 Go 基础工具库。设计初衷是打破 Go 严苛类型系统带来的繁琐摩擦,在处理数据时更关注需要什么类型而不是原本是什么类型。
|
||||||
|
|
||||||
* **万能零摩擦入口**:`To[T]、Convert` 作为核心 API,永不返回 `error`。在失败或非法转换时静默返回类型零值。
|
* **弱化类型摩擦**:转换函数在失败时返回合理零值,专注业务流。
|
||||||
* **语义化 As 包装**:提供 `As` 函数用于将传统“值+错误”双返回结果一键转化为单值,消除外部库带来的摩擦。
|
* **补足语言短板**:提供泛型工具类补足 Go 语言无三元运算等缺陷。
|
||||||
* **智能自动穿透**:`To[T]、Convert` 自动识别 JSON 文本、复杂容器映射、指针穿透等场景。
|
* **去标签化**:支持自动将 struct 字段名大写导出为小写,无需手动添加 JSON tag。
|
||||||
* **极致性能 JSON**:内置高性能 `fastjson` 引擎,支持 Struct 元数据缓存、`time.Time` 原生处理、零分配 Key 匹配及批量转义优化。
|
|
||||||
|
|
||||||
## 📦 安装
|
## 📦 安装
|
||||||
|
|
||||||
@ -23,83 +22,55 @@ go get apigo.cc/go/cast
|
|||||||
```go
|
```go
|
||||||
import "apigo.cc/go/cast"
|
import "apigo.cc/go/cast"
|
||||||
|
|
||||||
// 1. 万能转换 (To[T] 永不报错,遇错返回零值)
|
// 像 JS 一样转换
|
||||||
age := cast.To[int]("18") // 18
|
age := cast.Int("18") // 18
|
||||||
uid := cast.To[int]("abc") // 0 (静默失败)
|
|
||||||
|
|
||||||
// 2. 传统函数消除摩擦 (As 将双返回包装为单值)
|
// 泛型三元运算
|
||||||
b := cast.As(json.Marshal(data)) // 返回 []byte,忽略 error
|
status := cast.If(isAdmin, "Admin", "User")
|
||||||
|
|
||||||
// 3. 智能 JSON 自动转换
|
// JSON 序列化增强
|
||||||
// 输入 map/struct -> 目标 string: 自动序列化
|
var u struct { UserID int }
|
||||||
js := cast.To[string](map[string]int{"age": 18}) // `{"age":18}`
|
cast.UnmarshalJSON("userId: 1001", &u) // u.UserID = 1001
|
||||||
|
cast.MustToJSON(u) // "userId": 1001,首字母自动小写
|
||||||
|
|
||||||
// 输入 string/[]byte -> 目标 struct/map: 自动反序列化
|
// 字符串切分增强
|
||||||
user := cast.To[User](`{"name":"Tom"}`)
|
cast.Split(",", ",") // [],忽略无效切片
|
||||||
|
cast.Split("a, b, ", ",") // ["a", "b"],去除空白字符
|
||||||
|
|
||||||
// 4. 复杂容器转换 (Map/Slice)
|
|
||||||
m, _ := cast.ToMap[string, int]([]any{"id", "1"})
|
|
||||||
list, _ := cast.ToSlice[int]([]string{"1", "2", "3"})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠 API 指南
|
## 🛠 API 指南
|
||||||
|
|
||||||
### 核心 API
|
### 核心能力
|
||||||
|
|
||||||
1. **通用转换 (Frictionless)**
|
1. **基础转换(含多级指针穿透)**
|
||||||
* `To[T any](any) T` —— 万能转换入口。支持基础类型、Slice、Map 以及 JSON 的双向自动转换。失败时返回类型零值。
|
* `Int(any) int` | `Int64(any) int64`
|
||||||
* `Convert(dst, src any)` —— 原地转换。第一个参数为目标对象指针,支持深度映射、自动包装/解包、ParseHook 等。
|
* `Uint(any) uint` | `Uint64(any) uint64`
|
||||||
* 支持矩阵:
|
* `Float(any) float32` | `Float64(any) float64`
|
||||||
* `string/[]byte` <-> `struct/map/slice` (自动 JSON 编解码)
|
* `String(any) string` | `Bool(any) bool`
|
||||||
* `string (CSV)` -> `slice` (自动逗号分隔转换)
|
* `Duration(any) time.Duration`
|
||||||
* `map` <-> `struct` (字段名智能模糊匹配)
|
|
||||||
* `[]any` <-> `map` (KV 序列展开/折叠)
|
|
||||||
* `func` <-> `func` (自动参数类型适配转换)
|
|
||||||
* 所有基础类型 (`int`, `string`, `bool`, `float`, `duration`, `time.Time`) 互相转换。
|
|
||||||
|
|
||||||
2. **错误处理工具**
|
2. **批量转换(Slice Casting)**
|
||||||
* `As[T any](v T, err error) T` —— 错误消除工具。将传统的 `(value, error)` 返回值包装为单值。
|
* `Ints(any) []int64`
|
||||||
|
* `Strings(any) []string`
|
||||||
|
|
||||||
3. **容器转换 (Strict)**
|
3. **泛型工具(Type Helpers)**
|
||||||
* `ToMap[K, V](any) (map[K]V, error)` —— 构建新 Map。
|
* `If[T any](bool, T, T) T` —— 三元逻辑
|
||||||
* `ToSlice[T](any) ([]T, error)` —— 构建新 Slice。
|
* `In[T comparable]([]T, T) bool` —— 包含判断
|
||||||
* `FillMap(target, source)` | `FillSlice(target, source)` —— 填充现有容器。
|
* `Ptr[T any](T) *T` —— 取指针
|
||||||
|
* `ArrayToBoolMap[T comparable]([]T) map[T]bool` —— 快速索引化
|
||||||
|
|
||||||
4. **JSON 序列化与构建 (Strict)**
|
4. **序列化(JSON & YAML)**
|
||||||
* **编码**: `ToJSON(any) (string, error)` | `ToJSONBytes(any) ([]byte, error)`
|
* **JSON 编码**: `ToJSON(any)(string, error)` | `ToJSONDesensitize(any, []string)(string, error)` | `MustToJSON(any)string` | `PrettyToJSON(any)string`
|
||||||
* **解码**: `UnmarshalJSON(data, target) error` | `FromJSON[T](any) (T, error)`
|
* **JSON 字节**: `ToJSONBytes(any)([]byte, error)` | `ToJSONDesensitizeBytes(any, []string)([]byte, error)` | `MustJSONBytes(any)[]byte` | `PrettyToJSONBytes(any)[]byte`
|
||||||
* **时间支持**: 原生支持 `time.Time`。支持 Tag 格式定义:`json:"time_field,format=2006-01-02"`。
|
* **JSON 解码**: `UnmarshalJSON(string, any)(any, error)` | `MustUnmarshalJSON(string, any)any` | `UnmarshalJSONBytes([]byte, any)(any, error)` | `MustUnmarshalJSONBytes([]byte, any)any`
|
||||||
* **其他**: `ToJSONDesensitizeBytes(any, []string) ([]byte, error)` | `PrettyToJSON(any) string`
|
* **YAML 编码**: `ToYAML(any)(string, error)` | `MustToYAML(any)string` | `YAMLBytes(any)([]byte, error)` | `MustYAMLBytes(any)[]byte`
|
||||||
|
* **YAML 解码**: `UnmarshalYAML(string, any)(any, error)` | `MustUnmarshalYAML(string, any)any` | `UnmarshalYAMLBytes([]byte, any)(any, error)` | `MustUnmarshalYAMLBytes([]byte, any)any`
|
||||||
|
|
||||||
5. **泛型工具**
|
5. **辅助工具(Utilities)**
|
||||||
* `If[T any](bool, T, T) T` —— 三元逻辑。
|
* **切分**: `Split(s, sep) []string` | `SplitArgs(s) []string`
|
||||||
* `In[T comparable]([]T, T) bool` —— 包含判断。
|
* **拼接**: `UniqueAppend([]string, ...any) []string` | `JoinArgs([]string, sep) string`
|
||||||
* `Ptr[T any](T) *T` —— 快速取指针。
|
* **其他**: `RealValue(reflect.Value) reflect.Value` | `GetLowerName(string) string` | `GetUpperName(string) string` | `FixUpperCase([]byte, []string)`
|
||||||
* `ArrayToBoolMap[T comparable]([]T) map[T]bool` —— 快速构建索引 Map。
|
|
||||||
|
|
||||||
6. **基础转换与时间 (直接调用,极致性能)**
|
|
||||||
* `Int`, `Int64`, `Uint`, `Uint64`, `Float`, `Float64`, `String`, `Bool`, `Duration`
|
|
||||||
* `RealValue(reflect.Value) reflect.Value` —— 递归获取指针或接口下的真实值。
|
|
||||||
* `ParseTime(any) time.Time` —— 强大的时间解析,支持时间戳、RFC3339、JS 格式、紧凑格式及中文日期。
|
|
||||||
* `FormatTime(layout, any) string` —— 直观格式化(如 YYYY-MM-DD HH:mm:ss)。
|
|
||||||
* `AddTime(expr, any) time.Time` —— DSL 时间计算(如 +1Y-2M+3D)。
|
|
||||||
* `DescribeDuration(time.Duration) string` —— 将时长转化为自然语言描述(如 1h 5m)。
|
|
||||||
|
|
||||||
7. **字符串与参数处理**
|
|
||||||
* `Split(s, sep string) []string` —— 增强型分割,自动 Trim 并过滤空字符串。
|
|
||||||
* `SplitArgs(string) []string` —— 命令行参数分割,支持引号与转义。
|
|
||||||
* `JoinArgs([]string, sep string) string` —— 命令行参数合并,自动处理引号与转义。
|
|
||||||
* `UniqueAppend(to []string, from ...any) []string` —— 去重追加。
|
|
||||||
* `GetLowerName(string) string` | `GetUpperName(string) string` —— 首字母大小写转换工具。
|
|
||||||
* `FixUpperCase([]byte, []string)` —— 针对 JSON Key 的特殊大小写修复工具。
|
|
||||||
|
|
||||||
8. **时区支持**
|
|
||||||
* `DefaultTimeZone` —— 全局默认时区上下文。
|
|
||||||
* `SetDefaultTimeZone(*time.Location)` —— 修改全局默认时区(影响所有 Convert 与 ToTime 操作)。
|
|
||||||
* `DefaultTimeZone.Now()` —— 获取时区上下文下的当前时间。
|
|
||||||
* `NewTimeZone(*time.Location)` —— 创建一个时区上下文,支持 Parse、Format、Add、Now 等操作。
|
|
||||||
|
|
||||||
## 🧪 验证状态
|
## 🧪 验证状态
|
||||||
测试全部通过,性能达标。
|
测试全部通过,性能达标。详见:[TEST.md](./TEST.md)
|
||||||
|
|
||||||
详见:[TEST.md](./TEST.md)
|
|
||||||
|
|||||||
54
TEST.md
54
TEST.md
@ -1,44 +1,24 @@
|
|||||||
# 测试报告 (Test Report)
|
# 测试报告 (Test Report)
|
||||||
|
|
||||||
## 覆盖场景 (Coverage Scenarios)
|
## 覆盖场景 (Coverage Scenarios)
|
||||||
- **语义化转换**: `To[T]` 和 `As` 覆盖基础类型、Slice、Map 及智能 JSON 自动穿透转换。
|
- **核心类型转换**: `Int64`, `Uint64`, `Float64`, `Bool`, `String`,包括边界值、零值及非法字符串输入。
|
||||||
- **核心类型转换**: `Int64`, `Uint64`, `Float64`, `Bool`, `String`, `time.Time`,包括边界值、零值及非法字符串输入。
|
- **复合类型处理**: `Ints`, `Strings` 自动解析 JSON 字符串或直接转换。
|
||||||
- **时间类型原生支持**: 支持 `time.Time` 类型及自定义格式(通过 Struct Tag `format=` 指定)。
|
- **JSON/YAML 序列化**:
|
||||||
- **容器处理**: `ToMap`, `ToSlice`, `FillMap`, `FillSlice` 等泛型工具,支持 Struct 拍平与 KV 序列化。
|
- 深度结构体映射,支持 `FastEncoder` 单路径处理。
|
||||||
- **高性能 JSON 引擎**:
|
- **去标签化算法**: 自动识别 `UserID` -> `userID` 等符合工程习惯的转换。
|
||||||
- **元数据缓存**: 引入 `encoderStructDescriptor` 缓存,大幅减少反射开销。
|
- **脱敏支持**: `ToJSONDesensitize` 在编码阶段原生支持字段脱敏。
|
||||||
- **MapRange 遍历**: 优化 Map 序列化路径。
|
- **Map 兼容性**: 原生支持 `map[any]any` 及 Goja 伪数组转换。
|
||||||
- **批量转义**: `writeString` 优化,大幅提升长文本处理速度。
|
- **JSON 反序列化**:
|
||||||
- **脱敏支持**: `ToJSONDesensitizeBytes` 原生支持字段脱敏,并优化了脱敏路径的计算性能。
|
- **FastDecoder**: 实现单路径流式解析,跳过中间 Map 分配。
|
||||||
- **JSON 构建与解析**:
|
- **Frictionless 匹配**: 支持大小写不敏感、忽略下划线等灵活的 Key 映射规则。
|
||||||
- **零分配摩擦匹配**: `normalizeEqual` 算法实现 0 内存分配的归一化 Key 匹配,支持 UTF-8。
|
- **智能初始化**: 自动处理嵌套指针、Slice 和 Map 的初始化。
|
||||||
- **智能 Slice 扩容**: 尊重预设 Capacity,减少反序列化时的内存重分配。
|
|
||||||
- **FastDecoder**: 实现单路径流式解析,支持嵌套指针、Slice 和 Map 的智能初始化。
|
|
||||||
- **深度合并支持**: 支持在 `Convert` 过程中对 Map 和 Struct 进行深度递归合并。即使目标 Map 存储的是非指针结构体,也能通过反射寻址实现局部字段覆盖,而不丢失未提及的默认值。
|
|
||||||
- **指针与接口**: `RealValue` 处理多级指针与接口解包。
|
- **指针与接口**: `RealValue` 处理多级指针与接口解包。
|
||||||
- **实用工具**: `UniqueAppend` ($O(n)$ 去重),`If` (泛型三元),`SplitArgs` (支持引用格式)。
|
- **高性能实用函数**: `UniqueAppend` ($O(n)$ 去重),`If` (泛型三元),`SplitArgs` (支持引用格式)。
|
||||||
|
|
||||||
## 性能基准 (Benchmark Results - Intel(R) Core(TM) i9)
|
## 性能基准 (Benchmark Results - Intel(R) Core(TM) i9)
|
||||||
- `If`: ~0.24 ns/op (0 allocs/op)
|
- `If`: ~0.24 ns/op
|
||||||
- `Int64`: ~20 ns/op (0 allocs/op)
|
- `Int64`: ~20.4 ns/op
|
||||||
- `ToJSON (SimpleStruct)`: **~297 ns/op** (96 B/op) - 相比 v1.2.0 提升约 50%
|
- `ToJSON (SimpleStruct)`: ~448.5 ns/op (相比旧版提升 ~30%)
|
||||||
- `ToJSON (Map - No Sort)`: **~649 ns/op** (152 B/op) - 移除排序后性能大幅提升
|
- `ToJSON (DirtyMap)`: ~1126 ns/op (相比旧版提升 ~70%)
|
||||||
- `ToJSON (String Escaping)`: **~172 ns/op** (64 B/op) - 批量转义优化成果
|
- `UnmarshalJSON`: 高性能单路径解析,显著降低内存分配。
|
||||||
- `ToJSON (Time Support)`: **~522 ns/op** (232 B/op) - 原生 time.Time 处理
|
|
||||||
- `ParseTime (RFC3339)`: **~143 ns/op** (72 B/op) - 极致优化解析
|
|
||||||
- `ParseTime (Numeric String)`: **~188 ns/op** (0 B/op) - 紧凑格式零分配
|
|
||||||
- `ParseTime (Native Number)`: **~7 ns/op** (0 B/op) - 原生数字时间戳 0 分配 & 极致性能
|
|
||||||
- `FormatTime`: **~628 ns/op** (120 B/op) - 使用 Replacer 性能倍增
|
|
||||||
- `UnmarshalJSON (Frictionless)`: **~421 ns/op** (72 B/op) - 0 分配 Key 匹配
|
|
||||||
- `ToMap`: ~816 ns/op (含 Struct 拍平与类型转换)
|
|
||||||
- `ToSlice`: ~1819 ns/op
|
|
||||||
- `UniqueAppend`: 大数据量下的 $O(n)$ 时间复杂度。
|
- `UniqueAppend`: 大数据量下的 $O(n)$ 时间复杂度。
|
||||||
|
|
||||||
### 性能概览
|
|
||||||
| 场景 | 耗时 | 内存分配 | 说明 |
|
|
||||||
| :--- | :--- | :--- | :--- |
|
|
||||||
| **对象深度映射** | ~620 ns/op | 80 B/op | 引入 `performRecursiveTo` 引擎,就地赋值,无内存逃逸 |
|
|
||||||
| **Struct 序列化** | ~290 ns/op | 96 B/op | 引入元数据缓存,反射开销极低 |
|
|
||||||
| **Map 序列化** | ~650 ns/op | 152 B/op | 采用 MapRange + 无序化,性能翻倍 |
|
|
||||||
| **Frictionless 匹配** | ~420 ns/op | 72 B/op | 零分配 Key 比对,GC 压力极小 |
|
|
||||||
| **字符串转义** | ~170 ns/op | 64 B/op | 批量处理,长文本优势明显 |
|
|
||||||
|
|||||||
@ -36,7 +36,7 @@ func TestEdgeCases(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. JSON 异常输入
|
// 4. JSON 异常输入
|
||||||
if err := cast.UnmarshalJSON("{invalid_json}", nil); err == nil {
|
if _, err := cast.UnmarshalJSON("{invalid_json}", nil); err == nil {
|
||||||
t.Log("UnmarshalJSON handled invalid input")
|
t.Log("UnmarshalJSON handled invalid input")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,12 +66,12 @@ func BenchmarkString(b *testing.B) {
|
|||||||
|
|
||||||
func BenchmarkJSON(b *testing.B) {
|
func BenchmarkJSON(b *testing.B) {
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int
|
ID int `json:"id"`
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
u := User{ID: 1, Name: "Benchmark User"}
|
u := User{ID: 1, Name: "Benchmark User"}
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
_ = cast.To[string](u)
|
_ = cast.MustToJSON(u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,25 +88,6 @@ func BenchmarkSplit(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkToMap(b *testing.B) {
|
|
||||||
m := make(map[string]int)
|
|
||||||
kv := []any{"a", 1, "b", 2, "c", 3, "d", 4, "e", 5}
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
cast.FillMap(m, kv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkToSlice(b *testing.B) {
|
|
||||||
m := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}
|
|
||||||
var s []any
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
s = s[:0]
|
|
||||||
cast.FillSlice(&s, m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 测试 Int 转换的快速路径 (命中 switch case)
|
// 1. 测试 Int 转换的快速路径 (命中 switch case)
|
||||||
func BenchmarkInt_FastPath(b *testing.B) {
|
func BenchmarkInt_FastPath(b *testing.B) {
|
||||||
val := "123456"
|
val := "123456"
|
||||||
@ -140,13 +121,14 @@ func BenchmarkString_FastPath(b *testing.B) {
|
|||||||
// 4. 测试 ToJSON 性能 (对比自定义逻辑与标准库)
|
// 4. 测试 ToJSON 性能 (对比自定义逻辑与标准库)
|
||||||
func BenchmarkToJSON_SimpleStruct(b *testing.B) {
|
func BenchmarkToJSON_SimpleStruct(b *testing.B) {
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int
|
ID int `json:"id"`
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
u := User{ID: 1, Name: "Benchmark User"}
|
u := User{ID: 1, Name: "Benchmark User"}
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
_ = cast.To[string](u)
|
// 这里包含了你特有的 makeJSONType 清洗逻辑和 FixUpperCase 逻辑
|
||||||
|
_ = cast.MustToJSON(u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,24 +141,6 @@ func BenchmarkToJSON_DirtyMap(b *testing.B) {
|
|||||||
}
|
}
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
_ = cast.To[string](val)
|
_ = cast.MustToJSON(val)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkTo_StructToStruct(b *testing.B) {
|
|
||||||
type Source struct {
|
|
||||||
ID int
|
|
||||||
Name string
|
|
||||||
Age string
|
|
||||||
}
|
|
||||||
type Target struct {
|
|
||||||
ID int64
|
|
||||||
Name string
|
|
||||||
Age int
|
|
||||||
}
|
|
||||||
src := Source{ID: 1, Name: "Tom", Age: "18"}
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = cast.To[Target](src)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
771
cast.go
771
cast.go
@ -9,6 +9,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// If 泛型三元表达式
|
// If 泛型三元表达式
|
||||||
@ -24,15 +26,6 @@ func In[T comparable](arr []T, val T) bool {
|
|||||||
return slices.Contains(arr, val)
|
return slices.Contains(arr, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
// As 忽略错误,返回零值 (消除摩擦)
|
|
||||||
func As[T any](v T, err error) T {
|
|
||||||
if err != nil {
|
|
||||||
var zero T
|
|
||||||
return zero
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func RealValue(v reflect.Value) reflect.Value {
|
func RealValue(v reflect.Value) reflect.Value {
|
||||||
for v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface {
|
for v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface {
|
||||||
if v.IsNil() {
|
if v.IsNil() {
|
||||||
@ -45,494 +38,7 @@ func RealValue(v reflect.Value) reflect.Value {
|
|||||||
|
|
||||||
// --- Core Cast Logic ---
|
// --- Core Cast Logic ---
|
||||||
|
|
||||||
var (
|
|
||||||
timeType = reflect.TypeOf(time.Time{})
|
|
||||||
)
|
|
||||||
|
|
||||||
// To 深度转换 (支持基础类型、Slice、Map、Struct 及 JSON 自动转换,零摩擦模式)
|
|
||||||
func To[T any](v any) T {
|
|
||||||
var res T
|
|
||||||
performRecursiveTo(reflect.ValueOf(v), reflect.ValueOf(&res).Elem())
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert 深度转换 (支持基础类型、Slice、Map、Struct 及 JSON 自动转换,原地更新)
|
|
||||||
func Convert(dst, src any) {
|
|
||||||
if dst == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dstV := reflect.ValueOf(dst)
|
|
||||||
var srcV reflect.Value
|
|
||||||
if v, ok := src.(reflect.Value); ok {
|
|
||||||
srcV = v
|
|
||||||
} else {
|
|
||||||
srcV = reflect.ValueOf(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task 1: 拦截“自我转换”死循环陷阱 (Self-Assignment Guard)
|
|
||||||
if dstV.Kind() == reflect.Pointer && srcV.Kind() == reflect.Pointer {
|
|
||||||
if dstV.Pointer() == srcV.Pointer() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
performRecursiveTo(srcV, dstV)
|
|
||||||
}
|
|
||||||
|
|
||||||
func performRecursiveTo(src, dst reflect.Value) {
|
|
||||||
// 0. 处理 Interface 目标 (直接赋值)
|
|
||||||
if dst.Kind() == reflect.Interface {
|
|
||||||
if dst.CanSet() {
|
|
||||||
dst.Set(RealValue(src))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 处理指针 (自动初始化与解引用)
|
|
||||||
for dst.Kind() == reflect.Pointer {
|
|
||||||
if dst.IsNil() {
|
|
||||||
if dst.CanSet() {
|
|
||||||
dst.Set(reflect.New(dst.Type().Elem()))
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dst = dst.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 解开源数据
|
|
||||||
src = RealValue(src)
|
|
||||||
if !src.IsValid() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dstType := dst.Type()
|
|
||||||
|
|
||||||
// 3. 处理 JSON Unmarshaler 接口 (优先级高)
|
|
||||||
if dst.CanAddr() {
|
|
||||||
if unmarshaler, ok := dst.Addr().Interface().(json.Unmarshaler); ok {
|
|
||||||
var b []byte
|
|
||||||
if src.Kind() == reflect.String {
|
|
||||||
b = []byte(src.String())
|
|
||||||
} else if src.Kind() == reflect.Slice && src.Type().Elem().Kind() == reflect.Uint8 {
|
|
||||||
b = src.Bytes()
|
|
||||||
} else {
|
|
||||||
b, _ = ToJSONBytes(src.Interface())
|
|
||||||
}
|
|
||||||
if len(b) > 0 {
|
|
||||||
_ = unmarshaler.UnmarshalJSON(b)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Frictionless 增强:单值 <-> 切片 自动转换
|
|
||||||
if src.Kind() == reflect.Slice && src.Type().Elem().Kind() != reflect.Uint8 && src.Len() > 0 &&
|
|
||||||
dst.Kind() != reflect.Slice && dst.Kind() != reflect.Array && dst.Kind() != reflect.Map {
|
|
||||||
src = RealValue(src.Index(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 基础类型快速转换 (利用 cast 已有的高性能基础函数)
|
|
||||||
switch dst.Kind() {
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
if dst.CanSet() {
|
|
||||||
dst.SetInt(Int64(src.Interface()))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
||||||
if dst.CanSet() {
|
|
||||||
dst.SetUint(Uint64(src.Interface()))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
if dst.CanSet() {
|
|
||||||
dst.SetFloat(Float64(src.Interface()))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case reflect.String:
|
|
||||||
if dst.CanSet() {
|
|
||||||
if isComplexValue(src.Interface()) {
|
|
||||||
s, _ := ToJSON(src.Interface())
|
|
||||||
dst.SetString(s)
|
|
||||||
} else {
|
|
||||||
dst.SetString(String(src.Interface()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case reflect.Bool:
|
|
||||||
if dst.CanSet() {
|
|
||||||
dst.SetBool(Bool(src.Interface()))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 原生 time.Time 支持
|
|
||||||
if dstType == timeType {
|
|
||||||
if dst.CanSet() {
|
|
||||||
dst.Set(reflect.ValueOf(ToTime(src.Interface(), "")))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 复杂容器深度递归转换
|
|
||||||
switch dst.Kind() {
|
|
||||||
case reflect.Struct:
|
|
||||||
if src.Kind() == reflect.Map {
|
|
||||||
recursiveMapToStruct(src, dst)
|
|
||||||
} else if src.Kind() == reflect.Struct {
|
|
||||||
recursiveStructToStruct(src, dst)
|
|
||||||
} else if src.Kind() == reflect.String || (src.Kind() == reflect.Slice && src.Type().Elem().Kind() == reflect.Uint8) {
|
|
||||||
if dst.CanAddr() {
|
|
||||||
_ = UnmarshalJSON(src.Interface(), dst.Addr().Interface())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case reflect.Map:
|
|
||||||
if dst.IsNil() {
|
|
||||||
if dst.CanSet() {
|
|
||||||
dst.Set(reflect.MakeMap(dstType))
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if src.Kind() == reflect.Map {
|
|
||||||
recursiveMapToMap(src, dst)
|
|
||||||
} else if src.Kind() == reflect.Struct {
|
|
||||||
recursiveStructToMap(src, dst)
|
|
||||||
} else if src.Kind() == reflect.Slice || src.Kind() == reflect.Array {
|
|
||||||
recursiveSliceToMap(src, dst)
|
|
||||||
} else if src.Kind() == reflect.String {
|
|
||||||
if dst.CanAddr() {
|
|
||||||
_ = UnmarshalJSON(src.Interface(), dst.Addr().Interface())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case reflect.Slice:
|
|
||||||
if dstType.Elem().Kind() == reflect.Uint8 {
|
|
||||||
if dst.CanSet() {
|
|
||||||
if isComplexValue(src.Interface()) {
|
|
||||||
b, _ := ToJSONBytes(src.Interface())
|
|
||||||
dst.SetBytes(b)
|
|
||||||
} else {
|
|
||||||
dst.SetBytes(toBytes(src.Interface()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// JSON/CSV 字符串增强
|
|
||||||
if src.Kind() == reflect.String {
|
|
||||||
s := src.String()
|
|
||||||
if isJSONText(s) {
|
|
||||||
if dst.CanAddr() {
|
|
||||||
_ = UnmarshalJSON(s, dst.Addr().Interface())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(s, "[") && strings.Contains(s, ",") {
|
|
||||||
src = reflect.ValueOf(Split(s, ","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recursiveSliceToSlice(src, dst)
|
|
||||||
}
|
|
||||||
case reflect.Func:
|
|
||||||
if src.Kind() == reflect.Func {
|
|
||||||
dst.Set(reflect.MakeFunc(dstType, func(args []reflect.Value) []reflect.Value {
|
|
||||||
srcType := src.Type()
|
|
||||||
inParams := make([]reflect.Value, srcType.NumIn())
|
|
||||||
for i := 0; i < srcType.NumIn(); i++ {
|
|
||||||
inParams[i] = reflect.New(srcType.In(i)).Elem()
|
|
||||||
if i < len(args) {
|
|
||||||
performRecursiveTo(args[i], inParams[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results := src.Call(inParams)
|
|
||||||
outParams := make([]reflect.Value, dstType.NumOut())
|
|
||||||
for i := 0; i < dstType.NumOut(); i++ {
|
|
||||||
outParams[i] = reflect.New(dstType.Out(i)).Elem()
|
|
||||||
if i < len(results) {
|
|
||||||
performRecursiveTo(results[i], outParams[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return outParams
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内部递归助手 (零分配/高性能设计)
|
|
||||||
|
|
||||||
func recursiveMapToStruct(src, dst reflect.Value) {
|
|
||||||
if !dst.CanAddr() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dstAddr := dst.Addr()
|
|
||||||
dstType := dst.Type()
|
|
||||||
descriptor := getDecoderFieldMap(dstType)
|
|
||||||
iter := src.MapRange() // 零分配遍历
|
|
||||||
for iter.Next() {
|
|
||||||
rk := iter.Key()
|
|
||||||
var key string
|
|
||||||
if rk.Kind() == reflect.String {
|
|
||||||
key = rk.String()
|
|
||||||
} else {
|
|
||||||
key = String(rk.Interface())
|
|
||||||
}
|
|
||||||
|
|
||||||
if index, isTime, format, ok := matchField(key, descriptor); ok {
|
|
||||||
field := dst.Field(index)
|
|
||||||
fieldInfo := dstType.Field(index)
|
|
||||||
|
|
||||||
// ParseHook 支持: ParseFieldName(sourceType) FieldType
|
|
||||||
if method, ok := dstAddr.Type().MethodByName("Parse" + fieldInfo.Name); ok {
|
|
||||||
arg := reflect.New(method.Type.In(1)).Elem()
|
|
||||||
performRecursiveTo(iter.Value(), arg)
|
|
||||||
res := method.Func.Call([]reflect.Value{dstAddr, arg})
|
|
||||||
field.Set(res[0])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if isTime {
|
|
||||||
field.Set(reflect.ValueOf(ToTime(iter.Value().Interface(), format)))
|
|
||||||
} else {
|
|
||||||
performRecursiveTo(iter.Value(), field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func recursiveStructToStruct(src, dst reflect.Value) {
|
|
||||||
if !dst.CanAddr() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dstAddr := dst.Addr()
|
|
||||||
srcType := src.Type()
|
|
||||||
dstType := dst.Type()
|
|
||||||
descriptor := getDecoderFieldMap(dstType)
|
|
||||||
for i := 0; i < src.NumField(); i++ {
|
|
||||||
field := srcType.Field(i)
|
|
||||||
if !field.IsExported() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if field.Anonymous {
|
|
||||||
recursiveStructToStruct(src.Field(i), dst)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// 使用 matchField 实现归一化 Key 匹配 (零分配)
|
|
||||||
if index, isTime, format, ok := matchField(field.Name, descriptor); ok {
|
|
||||||
df := dst.Field(index)
|
|
||||||
fieldInfo := dstType.Field(index)
|
|
||||||
|
|
||||||
// ParseHook 支持
|
|
||||||
if method, ok := dstAddr.Type().MethodByName("Parse" + fieldInfo.Name); ok {
|
|
||||||
arg := reflect.New(method.Type.In(1)).Elem()
|
|
||||||
performRecursiveTo(src.Field(i), arg)
|
|
||||||
res := method.Func.Call([]reflect.Value{dstAddr, arg})
|
|
||||||
df.Set(res[0])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if isTime {
|
|
||||||
df.Set(reflect.ValueOf(ToTime(src.Field(i).Interface(), format)))
|
|
||||||
} else {
|
|
||||||
performRecursiveTo(src.Field(i), df)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func recursiveMapToMap(src, dst reflect.Value) {
|
|
||||||
kt := dst.Type().Key()
|
|
||||||
vt := dst.Type().Elem()
|
|
||||||
newKey := reflect.New(kt).Elem()
|
|
||||||
|
|
||||||
iter := src.MapRange()
|
|
||||||
for iter.Next() {
|
|
||||||
newKey.Set(reflect.Zero(kt))
|
|
||||||
performRecursiveTo(iter.Key(), newKey)
|
|
||||||
|
|
||||||
// 1. 创建一个【可寻址】的临时变量
|
|
||||||
newVal := reflect.New(vt).Elem()
|
|
||||||
|
|
||||||
// 2. 尝试从目标 Map 中获取已经存在的老值
|
|
||||||
existingVal := dst.MapIndex(newKey)
|
|
||||||
if existingVal.IsValid() {
|
|
||||||
// 如果老值存在,把它拷贝到我们刚才创建的临时变量里作“底本”
|
|
||||||
// 这样就能保留原本没被覆盖的字段
|
|
||||||
newVal.Set(existingVal)
|
|
||||||
} else {
|
|
||||||
// 如果老值不存在,就用零值兜底
|
|
||||||
newVal.Set(reflect.Zero(vt))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 对这个可寻址的临时变量进行深度合并
|
|
||||||
performRecursiveTo(iter.Value(), newVal)
|
|
||||||
|
|
||||||
// 4. 将合并后的全新值,重新塞回目标 Map
|
|
||||||
dst.SetMapIndex(newKey, newVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func recursiveStructToMap(src, dst reflect.Value) {
|
|
||||||
kt := dst.Type().Key()
|
|
||||||
vt := dst.Type().Elem()
|
|
||||||
newKey := reflect.New(kt).Elem()
|
|
||||||
|
|
||||||
srcType := src.Type()
|
|
||||||
for i := 0; i < src.NumField(); i++ {
|
|
||||||
field := srcType.Field(i)
|
|
||||||
if !field.IsExported() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if field.Anonymous {
|
|
||||||
recursiveStructToMap(src.Field(i), dst)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newKey.Set(reflect.Zero(kt))
|
|
||||||
performRecursiveTo(reflect.ValueOf(GetLowerName(field.Name)), newKey)
|
|
||||||
|
|
||||||
// 1. 创建一个【可寻址】的临时变量
|
|
||||||
newVal := reflect.New(vt).Elem()
|
|
||||||
|
|
||||||
// 2. 尝试获取老值
|
|
||||||
existingVal := dst.MapIndex(newKey)
|
|
||||||
if existingVal.IsValid() {
|
|
||||||
newVal.Set(existingVal)
|
|
||||||
} else {
|
|
||||||
newVal.Set(reflect.Zero(vt))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 合并
|
|
||||||
performRecursiveTo(src.Field(i), newVal)
|
|
||||||
|
|
||||||
// 4. 塞回
|
|
||||||
dst.SetMapIndex(newKey, newVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func recursiveSliceToMap(src, dst reflect.Value) {
|
|
||||||
kt := dst.Type().Key()
|
|
||||||
vt := dst.Type().Elem()
|
|
||||||
newKey := reflect.New(kt).Elem()
|
|
||||||
for i := 0; i < src.Len(); i += 2 {
|
|
||||||
newKey.Set(reflect.Zero(kt))
|
|
||||||
performRecursiveTo(src.Index(i), newKey)
|
|
||||||
|
|
||||||
newVal := reflect.New(vt).Elem()
|
|
||||||
existingVal := dst.MapIndex(newKey)
|
|
||||||
if existingVal.IsValid() {
|
|
||||||
newVal.Set(existingVal)
|
|
||||||
} else {
|
|
||||||
newVal.Set(reflect.Zero(vt))
|
|
||||||
}
|
|
||||||
|
|
||||||
if i+1 < src.Len() {
|
|
||||||
performRecursiveTo(src.Index(i+1), newVal)
|
|
||||||
}
|
|
||||||
dst.SetMapIndex(newKey, newVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func recursiveSliceToSlice(src, dst reflect.Value) {
|
|
||||||
et := dst.Type().Elem()
|
|
||||||
temp := reflect.New(et).Elem()
|
|
||||||
if src.Kind() == reflect.Slice || src.Kind() == reflect.Array {
|
|
||||||
l := src.Len()
|
|
||||||
dst.Set(reflect.MakeSlice(dst.Type(), l, l))
|
|
||||||
for i := 0; i < l; i++ {
|
|
||||||
temp.Set(reflect.Zero(et))
|
|
||||||
performRecursiveTo(src.Index(i), temp)
|
|
||||||
dst.Index(i).Set(temp)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Frictionless: 自动包装单值为切片 (覆盖赋值)
|
|
||||||
dst.Set(reflect.MakeSlice(dst.Type(), 1, 1))
|
|
||||||
temp.Set(reflect.Zero(et))
|
|
||||||
performRecursiveTo(src, temp)
|
|
||||||
dst.Index(0).Set(temp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToTime(v any, format string) time.Time {
|
|
||||||
if v == nil {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
if t, ok := v.(time.Time); ok {
|
|
||||||
return t.In(DefaultTimeZone.loc)
|
|
||||||
}
|
|
||||||
if format != "" {
|
|
||||||
s := String(v)
|
|
||||||
if t, err := time.ParseInLocation(format, s, DefaultTimeZone.loc); err == nil {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return DefaultTimeZone.ParseTime(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func reflectCast(value any, t reflect.Type) reflect.Value {
|
|
||||||
var v reflect.Value
|
|
||||||
if t.Kind() == reflect.Ptr {
|
|
||||||
v = reflect.New(t.Elem())
|
|
||||||
} else {
|
|
||||||
v = reflect.New(t).Elem()
|
|
||||||
}
|
|
||||||
performRecursiveTo(reflect.ValueOf(value), v)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func reflectCastE(value any, t reflect.Type) (reflect.Value, error) {
|
|
||||||
return reflectCast(value, t), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToInt64E(v any) (int64, error) {
|
|
||||||
return Int64(v), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToUint64E(v any) (uint64, error) {
|
|
||||||
return Uint64(v), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToFloat64E(v any) (float64, error) {
|
|
||||||
return Float64(v), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isJSONText(v any) bool {
|
|
||||||
switch val := v.(type) {
|
|
||||||
case string:
|
|
||||||
s := strings.TrimSpace(val)
|
|
||||||
return strings.HasPrefix(s, "{") || strings.HasPrefix(s, "[")
|
|
||||||
case []byte:
|
|
||||||
s := bytes.TrimSpace(val)
|
|
||||||
return bytes.HasPrefix(s, []byte("{")) || bytes.HasPrefix(s, []byte("["))
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isComplexType(t reflect.Type) bool {
|
|
||||||
kind := t.Kind()
|
|
||||||
for kind == reflect.Ptr {
|
|
||||||
t = t.Elem()
|
|
||||||
kind = t.Kind()
|
|
||||||
}
|
|
||||||
return kind == reflect.Struct || kind == reflect.Map || (kind == reflect.Slice && t.Elem().Kind() != reflect.Uint8)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isComplexValue(v any) bool {
|
|
||||||
if v == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
rv := RealValue(reflect.ValueOf(v))
|
|
||||||
if !rv.IsValid() || rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
kind := rv.Kind()
|
|
||||||
return kind == reflect.Struct || kind == reflect.Map || (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseInt(s string) int64 {
|
func parseInt(s string) int64 {
|
||||||
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
|
|
||||||
i, err := strconv.ParseInt(s, 0, 64)
|
|
||||||
if err == nil {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i, err := strconv.ParseInt(s, 10, 64)
|
i, err := strconv.ParseInt(s, 10, 64)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return i
|
return i
|
||||||
@ -544,12 +50,6 @@ func parseInt(s string) int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseUint(s string) uint64 {
|
func parseUint(s string) uint64 {
|
||||||
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
|
|
||||||
i, err := strconv.ParseUint(s, 0, 64)
|
|
||||||
if err == nil {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i, err := strconv.ParseUint(s, 10, 64)
|
i, err := strconv.ParseUint(s, 10, 64)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return i
|
return i
|
||||||
@ -565,49 +65,30 @@ func Int(value any) int { return int(Int64(value)) }
|
|||||||
// Helper for integer coercion to avoid repetition
|
// Helper for integer coercion to avoid repetition
|
||||||
func toInt64(value any) (int64, bool) {
|
func toInt64(value any) (int64, bool) {
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case int:
|
case int: return int64(v), true
|
||||||
return int64(v), true
|
case int8: return int64(v), true
|
||||||
case int8:
|
case int16: return int64(v), true
|
||||||
return int64(v), true
|
case int32: return int64(v), true
|
||||||
case int16:
|
case int64: return v, true
|
||||||
return int64(v), true
|
case uint: return int64(v), true
|
||||||
case int32:
|
case uint8: return int64(v), true
|
||||||
return int64(v), true
|
case uint16: return int64(v), true
|
||||||
case int64:
|
case uint32: return int64(v), true
|
||||||
return v, true
|
case uint64: return int64(v), true
|
||||||
case uint:
|
case float32: return int64(v), true
|
||||||
return int64(v), true
|
case float64: return int64(v), true
|
||||||
case uint8:
|
case bool: return If(v, int64(1), int64(0)), true
|
||||||
return int64(v), true
|
|
||||||
case uint16:
|
|
||||||
return int64(v), true
|
|
||||||
case uint32:
|
|
||||||
return int64(v), true
|
|
||||||
case uint64:
|
|
||||||
return int64(v), true
|
|
||||||
case float32:
|
|
||||||
return int64(v), true
|
|
||||||
case float64:
|
|
||||||
return int64(v), true
|
|
||||||
case bool:
|
|
||||||
return If(v, int64(1), int64(0)), true
|
|
||||||
}
|
}
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func Int64(value any) int64 {
|
func Int64(value any) int64 {
|
||||||
if value == nil {
|
if value == nil { return 0 }
|
||||||
return 0
|
if i, ok := toInt64(value); ok { return i }
|
||||||
}
|
|
||||||
if i, ok := toInt64(value); ok {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
switch realValue := value.(type) {
|
switch realValue := value.(type) {
|
||||||
case []byte:
|
case []byte: return parseInt(string(realValue))
|
||||||
return parseInt(string(realValue))
|
case string: return parseInt(realValue)
|
||||||
case string:
|
|
||||||
return parseInt(realValue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rv := reflect.ValueOf(value)
|
rv := reflect.ValueOf(value)
|
||||||
@ -733,16 +214,11 @@ func String(value any) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Bool(value any) bool {
|
func Bool(value any) bool {
|
||||||
if value == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if b, ok := value.(bool); ok {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
if i, ok := toInt64(value); ok {
|
|
||||||
return i != 0
|
|
||||||
}
|
|
||||||
switch realValue := value.(type) {
|
switch realValue := value.(type) {
|
||||||
|
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||||
|
return Uint64(realValue) != 0
|
||||||
|
case bool:
|
||||||
|
return realValue
|
||||||
case []byte:
|
case []byte:
|
||||||
s := strings.ToLower(string(realValue))
|
s := strings.ToLower(string(realValue))
|
||||||
return s == "1" || s == "t" || s == "true"
|
return s == "1" || s == "t" || s == "true"
|
||||||
@ -759,6 +235,57 @@ func Bool(value any) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Ints(value any) []int64 {
|
||||||
|
switch realValue := value.(type) {
|
||||||
|
case []any:
|
||||||
|
result := make([]int64, len(realValue))
|
||||||
|
for i, v := range realValue {
|
||||||
|
result[i] = Int64(v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
case string:
|
||||||
|
if strings.HasPrefix(realValue, "[") {
|
||||||
|
result := make([]int64, 0)
|
||||||
|
UnmarshalJSON(realValue, &result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return []int64{Int64(value)}
|
||||||
|
}
|
||||||
|
rv := reflect.ValueOf(value)
|
||||||
|
if rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface {
|
||||||
|
if rv = RealValue(rv); rv.IsValid() && rv.CanInterface() && rv.Kind() != reflect.Pointer {
|
||||||
|
return Ints(rv.Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []int64{Int64(value)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Strings(value any) []string {
|
||||||
|
switch realValue := value.(type) {
|
||||||
|
case []any:
|
||||||
|
result := make([]string, len(realValue))
|
||||||
|
for i, v := range realValue {
|
||||||
|
result[i] = String(v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
case string:
|
||||||
|
if strings.HasPrefix(realValue, "[") {
|
||||||
|
result := make([]string, 0)
|
||||||
|
UnmarshalJSON(realValue, &result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return []string{String(value)}
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := reflect.ValueOf(value)
|
||||||
|
if rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface {
|
||||||
|
if rv = RealValue(rv); rv.IsValid() && rv.CanInterface() && rv.Kind() != reflect.Pointer {
|
||||||
|
return Strings(rv.Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []string{String(value)}
|
||||||
|
}
|
||||||
|
|
||||||
func Duration(value any) time.Duration {
|
func Duration(value any) time.Duration {
|
||||||
if value == nil {
|
if value == nil {
|
||||||
return 0
|
return 0
|
||||||
@ -786,49 +313,110 @@ func ToJSONBytes(value any) ([]byte, error) {
|
|||||||
return fastToJSONBytes(value)
|
return fastToJSONBytes(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToJSONDesensitize(value any, keys []string) (string, error) {
|
||||||
|
b, err := fastToJSONBytes(value, keys...)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
func ToJSONDesensitizeBytes(value any, keys []string) ([]byte, error) {
|
func ToJSONDesensitizeBytes(value any, keys []string) ([]byte, error) {
|
||||||
return fastToJSONBytes(value, keys...)
|
return fastToJSONBytes(value, keys...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToJSON(value any) (string, error) {
|
func MustJSONBytes(value any) []byte {
|
||||||
b, err := ToJSONBytes(value)
|
j, err := ToJSONBytes(value)
|
||||||
return string(b), err
|
if err != nil {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
return j
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrettyToJSON(value any) string {
|
func ToJSON(value any) (string, error) {
|
||||||
j := As(ToJSONBytes(value))
|
j, err := ToJSONBytes(value)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(j), nil
|
||||||
|
}
|
||||||
|
func MustToJSON(value any) string { return string(MustJSONBytes(value)) }
|
||||||
|
|
||||||
|
func PrettyToJSONBytes(value any) []byte {
|
||||||
|
j := MustJSONBytes(value)
|
||||||
r := &bytes.Buffer{}
|
r := &bytes.Buffer{}
|
||||||
if err := json.Indent(r, j, "", " "); err == nil {
|
if err := json.Indent(r, j, "", " "); err == nil {
|
||||||
return r.String()
|
return r.Bytes()
|
||||||
}
|
}
|
||||||
return string(j)
|
return j
|
||||||
}
|
}
|
||||||
|
|
||||||
func toBytes(data any) []byte {
|
func PrettyToJSON(value any) string { return string(PrettyToJSONBytes(value)) }
|
||||||
if data == nil {
|
|
||||||
|
func UnmarshalJSONBytes(data []byte, value any) (any, error) {
|
||||||
|
if value == nil {
|
||||||
|
var v any
|
||||||
|
value = &v
|
||||||
|
}
|
||||||
|
err := fastUnmarshalJSONBytes(data, value)
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustUnmarshalJSONBytes(data []byte, value any) any {
|
||||||
|
v, err := UnmarshalJSONBytes(data, value)
|
||||||
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch v := data.(type) {
|
return v
|
||||||
case []byte:
|
|
||||||
return v
|
|
||||||
case string:
|
|
||||||
return []byte(v)
|
|
||||||
}
|
|
||||||
return []byte(String(data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func UnmarshalJSON(data any, value any) error {
|
func UnmarshalJSON(str string, value any) (any, error) { return UnmarshalJSONBytes([]byte(str), value) }
|
||||||
b := toBytes(data)
|
|
||||||
if b == nil {
|
func MustUnmarshalJSON(str string, value any) any { return MustUnmarshalJSONBytes([]byte(str), value) }
|
||||||
return fmt.Errorf("nil data")
|
|
||||||
|
func ToYAML(value any) (string, error) {
|
||||||
|
j, err := YAMLBytes(value)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
return fastUnmarshalJSONBytes(b, value)
|
return string(j), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromJSON[T any](data any) (T, error) {
|
func MustToYAML(value any) string { return string(MustYAMLBytes(value)) }
|
||||||
var v T
|
|
||||||
err := UnmarshalJSON(data, &v)
|
func UnmarshalYAML(data string, value any) (any, error) {
|
||||||
return v, err
|
return UnmarshalYAMLBytes([]byte(data), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustUnmarshalYAML(data string, value any) any { return MustUnmarshalYAMLBytes([]byte(data), value) }
|
||||||
|
|
||||||
|
func YAMLBytes(value any) ([]byte, error) {
|
||||||
|
j, err := yaml.Marshal(value)
|
||||||
|
if err == nil {
|
||||||
|
return j, nil
|
||||||
|
}
|
||||||
|
return []byte(fmt.Sprint(value)), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustYAMLBytes(value any) []byte {
|
||||||
|
j, err := YAMLBytes(value)
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnmarshalYAMLBytes(data []byte, value any) (any, error) {
|
||||||
|
err := yaml.Unmarshal(data, value)
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustUnmarshalYAMLBytes(data []byte, value any) any {
|
||||||
|
v, err := UnmarshalYAMLBytes(data, value)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Others (Keep logic but clean style) ---
|
// --- Others (Keep logic but clean style) ---
|
||||||
@ -928,45 +516,12 @@ func ArrayToBoolMap[T comparable](arr []T) map[T]bool {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMap 泛型构建新 Map
|
|
||||||
func ToMap[K comparable, V any](source any) (map[K]V, error) {
|
|
||||||
m := make(map[K]V)
|
|
||||||
performRecursiveTo(reflect.ValueOf(source), reflect.ValueOf(m))
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToSlice 泛型构建新 Slice
|
|
||||||
func ToSlice[T any](source any) ([]T, error) {
|
|
||||||
var s []T
|
|
||||||
performRecursiveTo(reflect.ValueOf(source), reflect.ValueOf(&s).Elem())
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FillMap 将 source 填充到目标 map 中
|
|
||||||
func FillMap(target any, source any) {
|
|
||||||
Convert(target, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FillSlice 将 source 填充到目标 slice 中
|
|
||||||
func FillSlice(target any, source any) {
|
|
||||||
Convert(target, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 补充缺失的 Key 转换工具
|
// 补充缺失的 Key 转换工具
|
||||||
func GetLowerName(s string) string {
|
func GetLowerName(s string) string {
|
||||||
if len(s) > 0 && s[0] >= 'A' && s[0] <= 'Z' {
|
if s == "" {
|
||||||
hasLower := false
|
return ""
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
if s[i] >= 'a' && s[i] <= 'z' {
|
|
||||||
hasLower = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hasLower {
|
|
||||||
return strings.ToLower(s[:1]) + s[1:]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return s
|
return strings.ToLower(s[:1]) + s[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUpperName(s string) string {
|
func GetUpperName(s string) string {
|
||||||
|
|||||||
41
cast_test.go
41
cast_test.go
@ -32,7 +32,7 @@ func TestStructToJSON(t *testing.T) {
|
|||||||
Age int
|
Age int
|
||||||
}
|
}
|
||||||
u := User{Name: "Tom", Age: 18}
|
u := User{Name: "Tom", Age: 18}
|
||||||
js := cast.To[string](u)
|
js := cast.MustToJSON(u)
|
||||||
// 验证首字母小写逻辑
|
// 验证首字母小写逻辑
|
||||||
if !strings.Contains(js, `"name":"Tom"`) || !strings.Contains(js, `"age":18`) {
|
if !strings.Contains(js, `"name":"Tom"`) || !strings.Contains(js, `"age":18`) {
|
||||||
t.Errorf("Struct to JSON auto-lowercase failed: %s", js)
|
t.Errorf("Struct to JSON auto-lowercase failed: %s", js)
|
||||||
@ -52,16 +52,27 @@ func TestJSONToStruct(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestYAMLConversion(t *testing.T) {
|
||||||
|
type Config struct {
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
c := Config{Port: 8080}
|
||||||
|
yamlStr := cast.MustToYAML(c)
|
||||||
|
if !strings.Contains(yamlStr, "port: 8080") {
|
||||||
|
t.Errorf("YAML conversion failed: %s", yamlStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSpecialJSON(t *testing.T) {
|
func TestSpecialJSON(t *testing.T) {
|
||||||
// 关键测试:特殊 HTML 字符序列化不应被转义
|
// 关键测试:特殊 HTML 字符序列化不应被转义
|
||||||
type Content struct {
|
type Content struct {
|
||||||
Text string
|
Text string `json:"text"`
|
||||||
}
|
}
|
||||||
c := Content{Text: "<a> & <b>"}
|
c := Content{Text: "<a> & <b>"}
|
||||||
|
|
||||||
// 标准 json.Marshal 会变成 "<a> \u0026 <b>"
|
// 标准 json.Marshal 会变成 "<a> \u0026 <b>"
|
||||||
// 我们期望输出原始字符 "<a> & <b>"
|
// 我们期望输出原始字符 "<a> & <b>"
|
||||||
js := cast.To[string](c)
|
js := cast.MustToJSON(c)
|
||||||
expected := `{"text":"<a> & <b>"}`
|
expected := `{"text":"<a> & <b>"}`
|
||||||
if js != expected {
|
if js != expected {
|
||||||
t.Errorf("Special JSON failed.\nExpected: %s\nActual: %s", expected, js)
|
t.Errorf("Special JSON failed.\nExpected: %s\nActual: %s", expected, js)
|
||||||
@ -84,7 +95,7 @@ func TestComplexJSONType(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
js := cast.To[string](data)
|
js := cast.MustToJSON(data)
|
||||||
// 期望 123 被转为 "123" 且内容正确
|
// 期望 123 被转为 "123" 且内容正确
|
||||||
if !strings.Contains(js, `"123":"numeric key"`) || !strings.Contains(js, `"ok":true`) {
|
if !strings.Contains(js, `"123":"numeric key"`) || !strings.Contains(js, `"ok":true`) {
|
||||||
t.Errorf("Complex JSON type failed: %s", js)
|
t.Errorf("Complex JSON type failed: %s", js)
|
||||||
@ -138,7 +149,7 @@ func TestGojaMapToArrayFallback(t *testing.T) {
|
|||||||
"2": "cherry", // 字符串形式的数字也应该被正确转换
|
"2": "cherry", // 字符串形式的数字也应该被正确转换
|
||||||
}
|
}
|
||||||
|
|
||||||
js := cast.To[string](mockGojaArray)
|
js := cast.MustToJSON(mockGojaArray)
|
||||||
|
|
||||||
// 我们期望它被正确识别并转化为标准的 JSON 数组,且顺序不会乱
|
// 我们期望它被正确识别并转化为标准的 JSON 数组,且顺序不会乱
|
||||||
expected := `["apple","banana","cherry"]`
|
expected := `["apple","banana","cherry"]`
|
||||||
@ -194,7 +205,7 @@ func TestUnaddressableStruct(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
res := cast.To[string](d)
|
res := cast.MustToJSON(d)
|
||||||
if !strings.Contains(res, "\"name\"") {
|
if !strings.Contains(res, "\"name\"") {
|
||||||
t.Errorf("Value struct ToJSON failed to lowercase key: %s", res)
|
t.Errorf("Value struct ToJSON failed to lowercase key: %s", res)
|
||||||
}
|
}
|
||||||
@ -203,24 +214,24 @@ func TestUnaddressableStruct(t *testing.T) {
|
|||||||
func TestToJSON_Nil(t *testing.T) {
|
func TestToJSON_Nil(t *testing.T) {
|
||||||
// Nil slice should be []
|
// Nil slice should be []
|
||||||
var s []int
|
var s []int
|
||||||
if js := cast.To[string](s); js != "[]" {
|
if js := cast.MustToJSON(s); js != "[]" {
|
||||||
t.Errorf("Nil slice expected [], got %s", js)
|
t.Errorf("Nil slice expected [], got %s", js)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nil map should be {}
|
// Nil map should be {}
|
||||||
var m map[string]int
|
var m map[string]int
|
||||||
if js := cast.To[string](m); js != "{}" {
|
if js := cast.MustToJSON(m); js != "{}" {
|
||||||
t.Errorf("Nil map expected {}, got %s", js)
|
t.Errorf("Nil map expected {}, got %s", js)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nil pointer should be null
|
// Nil pointer should be null
|
||||||
var p *int
|
var p *int
|
||||||
if js := cast.To[string](p); js != "null" {
|
if js := cast.MustToJSON(p); js != "null" {
|
||||||
t.Errorf("Nil pointer expected null, got %s", js)
|
t.Errorf("Nil pointer expected null, got %s", js)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestToJSONDesensitizeBytes(t *testing.T) {
|
func TestToJSONDesensitize(t *testing.T) {
|
||||||
type User struct {
|
type User struct {
|
||||||
Name string
|
Name string
|
||||||
Password string
|
Password string
|
||||||
@ -229,8 +240,10 @@ func TestToJSONDesensitizeBytes(t *testing.T) {
|
|||||||
u := User{Name: "Tom", Password: "secret123", Age: 18}
|
u := User{Name: "Tom", Password: "secret123", Age: 18}
|
||||||
|
|
||||||
// 测试脱敏功能
|
// 测试脱敏功能
|
||||||
b := cast.As(cast.ToJSONDesensitizeBytes(u, []string{"password"}))
|
js, err := cast.ToJSONDesensitize(u, []string{"password"})
|
||||||
js := string(b)
|
if err != nil {
|
||||||
|
t.Fatalf("ToJSONDesensitize failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if !strings.Contains(js, `"password":"***"`) {
|
if !strings.Contains(js, `"password":"***"`) {
|
||||||
t.Errorf("Password should be desensitized, got: %s", js)
|
t.Errorf("Password should be desensitized, got: %s", js)
|
||||||
@ -245,7 +258,7 @@ func TestFastEncoder_MapAny(t *testing.T) {
|
|||||||
"userName": "admin",
|
"userName": "admin",
|
||||||
123: "val",
|
123: "val",
|
||||||
}
|
}
|
||||||
js := cast.To[string](data)
|
js := cast.MustToJSON(data)
|
||||||
if !strings.Contains(js, `"123":"val"`) || !strings.Contains(js, `"userName":"admin"`) {
|
if !strings.Contains(js, `"123":"val"`) || !strings.Contains(js, `"userName":"admin"`) {
|
||||||
t.Errorf("MapAny encoding failed: %s", js)
|
t.Errorf("MapAny encoding failed: %s", js)
|
||||||
}
|
}
|
||||||
@ -256,7 +269,7 @@ func TestFastEncoder_GojaArray(t *testing.T) {
|
|||||||
0: "a",
|
0: "a",
|
||||||
1: "b",
|
1: "b",
|
||||||
}
|
}
|
||||||
js := cast.To[string](data)
|
js := cast.MustToJSON(data)
|
||||||
if js != `["a","b"]` {
|
if js != `["a","b"]` {
|
||||||
t.Errorf("Goja array fallback failed: %s", js)
|
t.Errorf("Goja array fallback failed: %s", js)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,102 +0,0 @@
|
|||||||
package cast_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SubConfig struct {
|
|
||||||
Level int
|
|
||||||
Tag string
|
|
||||||
}
|
|
||||||
|
|
||||||
type MainConfig struct {
|
|
||||||
Name string
|
|
||||||
Sub SubConfig
|
|
||||||
Items []string
|
|
||||||
Options map[string]int
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeepMergeComplex(t *testing.T) {
|
|
||||||
dst := MainConfig{
|
|
||||||
Name: "Base",
|
|
||||||
Sub: SubConfig{
|
|
||||||
Level: 1,
|
|
||||||
Tag: "original",
|
|
||||||
},
|
|
||||||
Items: []string{"a", "b"},
|
|
||||||
Options: map[string]int{
|
|
||||||
"debug": 1,
|
|
||||||
"trace": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
src := map[string]any{
|
|
||||||
"Sub": map[string]any{
|
|
||||||
"Level": 2,
|
|
||||||
},
|
|
||||||
"Options": map[string]any{
|
|
||||||
"trace": 1,
|
|
||||||
"new": 100,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cast.Convert(&dst, src)
|
|
||||||
|
|
||||||
if dst.Name != "Base" {
|
|
||||||
t.Errorf("Expected Name Base, got %s", dst.Name)
|
|
||||||
}
|
|
||||||
if dst.Sub.Level != 2 {
|
|
||||||
t.Errorf("Expected Sub.Level 2, got %d", dst.Sub.Level)
|
|
||||||
}
|
|
||||||
if dst.Sub.Tag != "original" {
|
|
||||||
t.Errorf("Expected Sub.Tag original, got %s", dst.Sub.Tag)
|
|
||||||
}
|
|
||||||
if len(dst.Items) != 2 {
|
|
||||||
t.Errorf("Expected Items length 2, got %d", len(dst.Items))
|
|
||||||
}
|
|
||||||
if dst.Options["debug"] != 1 {
|
|
||||||
t.Errorf("Expected Options.debug 1, got %d", dst.Options["debug"])
|
|
||||||
}
|
|
||||||
if dst.Options["trace"] != 1 {
|
|
||||||
t.Errorf("Expected Options.trace 1, got %d", dst.Options["trace"])
|
|
||||||
}
|
|
||||||
if dst.Options["new"] != 100 {
|
|
||||||
t.Errorf("Expected Options.new 100, got %d", dst.Options["new"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapToMapMergeComplex(t *testing.T) {
|
|
||||||
dst := map[string]MainConfig{
|
|
||||||
"c1": {
|
|
||||||
Name: "Config1",
|
|
||||||
Sub: SubConfig{Level: 10},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
src := map[string]any{
|
|
||||||
"c1": map[string]any{
|
|
||||||
"Sub": map[string]any{
|
|
||||||
"Tag": "updated",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"c2": map[string]any{
|
|
||||||
"Name": "Config2",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cast.Convert(&dst, src)
|
|
||||||
|
|
||||||
if dst["c1"].Name != "Config1" {
|
|
||||||
t.Errorf("Expected c1.Name Config1, got %s", dst["c1"].Name)
|
|
||||||
}
|
|
||||||
if dst["c1"].Sub.Level != 10 {
|
|
||||||
t.Errorf("Expected c1.Sub.Level 10, got %d", dst["c1"].Sub.Level)
|
|
||||||
}
|
|
||||||
if dst["c1"].Sub.Tag != "updated" {
|
|
||||||
t.Errorf("Expected c1.Sub.Tag updated, got %s", dst["c1"].Sub.Tag)
|
|
||||||
}
|
|
||||||
if dst["c2"].Name != "Config2" {
|
|
||||||
t.Errorf("Expected c2.Name Config2, got %s", dst["c2"].Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
package cast_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestToMap(t *testing.T) {
|
|
||||||
// Test struct-to-map with inheritance
|
|
||||||
type Base struct {
|
|
||||||
ID int
|
|
||||||
}
|
|
||||||
type User struct {
|
|
||||||
Base
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
u := User{Base: Base{ID: 1}, Name: "Tom"}
|
|
||||||
m1 := make(map[string]any)
|
|
||||||
cast.FillMap(m1, u)
|
|
||||||
if m1["ID"] != 1 || m1["name"] != "Tom" {
|
|
||||||
t.Errorf("Struct inheritance to map failed: %v", m1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test slice-to-map (KV)
|
|
||||||
m2 := cast.As(cast.ToMap[int, string]([]any{"1", "a", 2, 200}))
|
|
||||||
if m2[1] != "a" || m2[2] != "200" {
|
|
||||||
t.Errorf("Slice-to-map failed: %v", m2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToSlice(t *testing.T) {
|
|
||||||
// Test primitive-to-slice (overwrite)
|
|
||||||
var s1 []int
|
|
||||||
cast.FillSlice(&s1, "123")
|
|
||||||
if len(s1) != 1 || s1[0] != 123 {
|
|
||||||
t.Errorf("Primitive-to-slice failed: %v", s1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test slice-to-slice (overwrite)
|
|
||||||
cast.FillSlice(&s1, []string{"456"})
|
|
||||||
if len(s1) != 1 || s1[0] != 456 {
|
|
||||||
t.Errorf("Slice-to-slice overwrite failed: %v", s1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJSON(t *testing.T) {
|
|
||||||
type Config struct {
|
|
||||||
Port int
|
|
||||||
}
|
|
||||||
data := `{"port": 8080}`
|
|
||||||
|
|
||||||
// Test UnmarshalJSON
|
|
||||||
var c1 Config
|
|
||||||
err := cast.UnmarshalJSON(data, &c1)
|
|
||||||
if err != nil || c1.Port != 8080 {
|
|
||||||
t.Errorf("UnmarshalJSON failed: %v, %v", err, c1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test FromJSON
|
|
||||||
c2 := cast.As(cast.FromJSON[Config]([]byte(data)))
|
|
||||||
if c2.Port != 8080 {
|
|
||||||
t.Errorf("FromJSON failed: %v", c2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test To[T] from JSON
|
|
||||||
c3 := cast.To[Config](data)
|
|
||||||
if c3.Port != 8080 {
|
|
||||||
t.Errorf("To[Config] failed: %v", c3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenericToSlice(t *testing.T) {
|
|
||||||
// Test To with various inputs
|
|
||||||
s1 := cast.To[[]int]("123")
|
|
||||||
if len(s1) != 1 || s1[0] != 123 {
|
|
||||||
t.Errorf("To[[]int] primitive failed: %v", s1)
|
|
||||||
}
|
|
||||||
|
|
||||||
s2 := cast.To[[]string]([]int{1, 2})
|
|
||||||
if len(s2) != 2 || s2[0] != "1" || s2[1] != "2" {
|
|
||||||
t.Errorf("To[[]string] slice failed: %v", s2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,200 +0,0 @@
|
|||||||
package cast_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Matrix Test Entry
|
|
||||||
func TestConvertMatrix(t *testing.T) {
|
|
||||||
type testCase struct {
|
|
||||||
name string
|
|
||||||
from any
|
|
||||||
to any // 传入指针的指针,用于接收结果
|
|
||||||
expected any
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 基础类型全互转矩阵
|
|
||||||
cases := []testCase{
|
|
||||||
{name: "int to string", from: 123, to: new(string), expected: "123"},
|
|
||||||
{name: "string to int", from: "456", to: new(int), expected: 456},
|
|
||||||
{name: "float to int", from: 123.45, to: new(int), expected: 123},
|
|
||||||
{name: "bool to string", from: true, to: new(string), expected: "true"},
|
|
||||||
{name: "string to bool", from: "1", to: new(bool), expected: true},
|
|
||||||
{name: "string to bool (text)", from: "true", to: new(bool), expected: true},
|
|
||||||
|
|
||||||
// 2. 容器与单值的自动包装/解包 (去摩擦)
|
|
||||||
{name: "single to slice", from: 100, to: new([]int), expected: []int{100}},
|
|
||||||
{name: "slice to single (len 1)", from: []int{200}, to: new(int), expected: 200},
|
|
||||||
{name: "slice to single (len >1, take first)", from: []int{300, 400}, to: new(int), expected: 300},
|
|
||||||
|
|
||||||
// 3. 字符串与切片的智能转换
|
|
||||||
{name: "csv string to slice", from: "1, 2, 3", to: new([]int), expected: []int{1, 2, 3}},
|
|
||||||
{name: "json string to slice", from: `[4, 5, 6]`, to: new([]int), expected: []int{4, 5, 6}},
|
|
||||||
|
|
||||||
// 4. 指针转换 (深度穿透)
|
|
||||||
{name: "deep pointer to val", from: ptr(ptr(789)), to: new(int), expected: 789},
|
|
||||||
{name: "val to pointer", from: 999, to: new(*int), expected: ptr(999)},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
cast.Convert(tc.to, tc.from)
|
|
||||||
// 获取 to 指针指向的实际值
|
|
||||||
actual := reflect.ValueOf(tc.to).Elem().Interface()
|
|
||||||
if !reflect.DeepEqual(actual, tc.expected) {
|
|
||||||
t.Errorf("[%s] Failed: from(%v) expected(%v) but got(%v)", tc.name, tc.from, tc.expected, actual)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvert_SelfAssignment(t *testing.T) {
|
|
||||||
type User struct {
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
u := &User{Name: "Tom"}
|
|
||||||
// 应命中自我转换保护,不报错且不改变状态
|
|
||||||
cast.Convert(u, u)
|
|
||||||
if u.Name != "Tom" {
|
|
||||||
t.Error("Self-assignment guard failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 复杂映射与去摩擦 Key 匹配测试
|
|
||||||
func TestComplexFrictionlessMapping(t *testing.T) {
|
|
||||||
// 场景:Map 键名非常混乱,Struct 嵌套,目标存在旧数据
|
|
||||||
type Sub struct {
|
|
||||||
Age int
|
|
||||||
}
|
|
||||||
type User struct {
|
|
||||||
UserID int
|
|
||||||
UserName string
|
|
||||||
SubInfo Sub
|
|
||||||
}
|
|
||||||
|
|
||||||
from := map[string]any{
|
|
||||||
"user_id": 1001, // 下划线
|
|
||||||
"USER-NAME": "Andy", // 中划线+大写
|
|
||||||
"subinfo": map[string]any{ // 嵌套+全小写
|
|
||||||
"age": "18", // 类型不一致 (string -> int)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var to User
|
|
||||||
to.UserID = 999 // 预设旧数据,验证是否被正确覆盖
|
|
||||||
cast.Convert(&to, from)
|
|
||||||
|
|
||||||
if to.UserID != 1001 {
|
|
||||||
t.Errorf("UserID match failed: %d", to.UserID)
|
|
||||||
}
|
|
||||||
if to.UserName != "Andy" {
|
|
||||||
t.Errorf("UserName match failed: %s", to.UserName)
|
|
||||||
}
|
|
||||||
if to.SubInfo.Age != 18 {
|
|
||||||
t.Errorf("SubInfo.Age match failed: %d", to.SubInfo.Age)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 函数转换测试 (Func to Func)
|
|
||||||
func TestFuncConversion(t *testing.T) {
|
|
||||||
// 源函数:接收 int, string,返回 int, string
|
|
||||||
f1 := func(a int, b string) (int, string) {
|
|
||||||
return a + 1, b + "!"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 目标函数:意图是接收 string, any,返回 string, any
|
|
||||||
var f2 func(string, any) (string, any)
|
|
||||||
cast.Convert(&f2, f1)
|
|
||||||
|
|
||||||
if f2 == nil {
|
|
||||||
t.Fatal("Converted function is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
r1, r2 := f2("10", "hello")
|
|
||||||
if r1 != "11" || r2 != "hello!" {
|
|
||||||
t.Errorf("Func conversion failed: r1=%v, r2=%v", r1, r2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. 命名风格模糊匹配测试
|
|
||||||
func TestFuzzyNamingMapping(t *testing.T) {
|
|
||||||
type TestStruct struct {
|
|
||||||
JSONTag string
|
|
||||||
UserID int
|
|
||||||
UserName string
|
|
||||||
IsActiveFlag bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试用例:各种不规范输入到标准结构体的映射
|
|
||||||
from := map[string]any{
|
|
||||||
"jsontag": "val1", // 小写
|
|
||||||
"JSONTag": "val2", // 原名
|
|
||||||
"JSON_TAG": "val3", // 下划线大写
|
|
||||||
"user_id": 123, // 下划线小写
|
|
||||||
"USERID": 456, // 全大写
|
|
||||||
"userName": "Andy", // 首字母小写
|
|
||||||
"user-name": "Bob", // 中划线
|
|
||||||
"is_active_flag": "true", // 模糊匹配 bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var to TestStruct
|
|
||||||
cast.Convert(&to, from)
|
|
||||||
|
|
||||||
if to.UserID == 0 {
|
|
||||||
t.Errorf("UserID should be matched and set")
|
|
||||||
}
|
|
||||||
if to.UserName == "" {
|
|
||||||
t.Errorf("UserName should be matched and set")
|
|
||||||
}
|
|
||||||
if !to.IsActiveFlag {
|
|
||||||
t.Errorf("IsActiveFlag should be set to true")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Conversion result: %+v", to)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. 异常防御测试 (Panic Prevention)
|
|
||||||
func TestPanicPrevention(t *testing.T) {
|
|
||||||
t.Run("nil destination", func(t *testing.T) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
t.Errorf("Panic occurred with nil destination: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
cast.Convert(nil, "data")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("nil pointer destination", func(t *testing.T) {
|
|
||||||
// nil 指针目标应该是可以接受的,库应当忽略或优雅处理,不应 panic
|
|
||||||
var p *int = nil
|
|
||||||
cast.Convert(&p, 123) // 这里传入的是指针的指针
|
|
||||||
if p == nil || *p != 123 {
|
|
||||||
t.Errorf("Expected p to be initialized and set to 123, got %v", p)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("read-only destination", func(t *testing.T) {
|
|
||||||
const i = 1
|
|
||||||
cast.Convert(i, 123)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数
|
|
||||||
func ptr[T any](v T) *T { return &v }
|
|
||||||
|
|
||||||
// --- 性能测试 ---
|
|
||||||
|
|
||||||
func BenchmarkCastMatrixConvert(b *testing.B) {
|
|
||||||
from := map[string]any{"id": "123", "name": "test"}
|
|
||||||
type Target struct {
|
|
||||||
ID int
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
var to Target
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
cast.Convert(&to, from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,7 +15,7 @@ func TestFastUnmarshal_Frictionless(t *testing.T) {
|
|||||||
// 测试各种 Key 格式的匹配
|
// 测试各种 Key 格式的匹配
|
||||||
data := `{"user_id": 1001, "UserName": "Tom", "isadmin": "true"}`
|
data := `{"user_id": 1001, "UserName": "Tom", "isadmin": "true"}`
|
||||||
var u User
|
var u User
|
||||||
err := cast.UnmarshalJSON(data, &u)
|
_, err := cast.UnmarshalJSON(data, &u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("UnmarshalJSON failed: %v", err)
|
t.Fatalf("UnmarshalJSON failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -1,3 +1,5 @@
|
|||||||
module apigo.cc/go/cast
|
module apigo.cc/go/cast
|
||||||
|
|
||||||
go 1.25
|
go 1.25
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|||||||
190
json_decoder.go
190
json_decoder.go
@ -8,25 +8,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
structFieldMapCache sync.Map
|
structFieldMapCache sync.Map
|
||||||
)
|
)
|
||||||
|
|
||||||
type decoderFieldDescriptor struct {
|
|
||||||
index int
|
|
||||||
isTime bool
|
|
||||||
timeFormat string
|
|
||||||
normalized string
|
|
||||||
}
|
|
||||||
|
|
||||||
type decoderStructDescriptor struct {
|
|
||||||
exactMatches map[string]decoderFieldDescriptor
|
|
||||||
fields []decoderFieldDescriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
type decoder struct {
|
type decoder struct {
|
||||||
data []byte
|
data []byte
|
||||||
pos int
|
pos int
|
||||||
@ -49,10 +36,10 @@ func (d *decoder) decode(value any) error {
|
|||||||
return errors.New("destination must be a non-nil pointer")
|
return errors.New("destination must be a non-nil pointer")
|
||||||
}
|
}
|
||||||
d.skipWhitespace()
|
d.skipWhitespace()
|
||||||
return d.decodeValue(reflectValue.Elem(), "")
|
return d.decodeValue(reflectValue.Elem())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *decoder) decodeValue(reflectValue reflect.Value, timeFormat string) error {
|
func (d *decoder) decodeValue(reflectValue reflect.Value) error {
|
||||||
d.skipWhitespace()
|
d.skipWhitespace()
|
||||||
if d.pos >= len(d.data) {
|
if d.pos >= len(d.data) {
|
||||||
return nil
|
return nil
|
||||||
@ -75,52 +62,10 @@ func (d *decoder) decodeValue(reflectValue reflect.Value, timeFormat string) err
|
|||||||
reflectValue = reflectValue.Elem()
|
reflectValue = reflectValue.Elem()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 time.Time
|
|
||||||
if reflectValue.Type() == timeType {
|
|
||||||
var v any
|
|
||||||
if char == '"' {
|
|
||||||
str, err := d.parseString()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v = str
|
|
||||||
} else if char == 'n' {
|
|
||||||
if string(d.data[d.pos:min(d.pos+4, len(d.data))]) == "null" {
|
|
||||||
d.pos += 4
|
|
||||||
reflectValue.Set(reflect.Zero(reflectValue.Type()))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
literal, err := d.parseLiteral()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v = literal
|
|
||||||
}
|
|
||||||
reflectValue.Set(reflect.ValueOf(ToTime(v, timeFormat)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch char {
|
switch char {
|
||||||
case '{':
|
case '{':
|
||||||
if reflectValue.Kind() == reflect.Interface {
|
|
||||||
m := make(map[string]any)
|
|
||||||
if err := d.decodeObject(reflect.ValueOf(&m).Elem()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
reflectValue.Set(reflect.ValueOf(m))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return d.decodeObject(reflectValue)
|
return d.decodeObject(reflectValue)
|
||||||
case '[':
|
case '[':
|
||||||
if reflectValue.Kind() == reflect.Interface {
|
|
||||||
var s []any
|
|
||||||
if err := d.decodeArray(reflect.ValueOf(&s).Elem()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
reflectValue.Set(reflect.ValueOf(s))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return d.decodeArray(reflectValue)
|
return d.decodeArray(reflectValue)
|
||||||
case '"':
|
case '"':
|
||||||
str, err := d.parseString()
|
str, err := d.parseString()
|
||||||
@ -139,13 +84,11 @@ func (d *decoder) decodeValue(reflectValue reflect.Value, timeFormat string) err
|
|||||||
reflectValue.SetFloat(Float64(str))
|
reflectValue.SetFloat(Float64(str))
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
reflectValue.SetBool(Bool(str))
|
reflectValue.SetBool(Bool(str))
|
||||||
case reflect.Interface:
|
|
||||||
reflectValue.Set(reflect.ValueOf(str))
|
|
||||||
default:
|
default:
|
||||||
// 尝试将字符串解析为具体对象(比如内部又是 JSON)
|
// 尝试将字符串解析为具体对象(比如内部又是 JSON)
|
||||||
if strings.HasPrefix(str, "{") || strings.HasPrefix(str, "[") {
|
if strings.HasPrefix(str, "{") || strings.HasPrefix(str, "[") {
|
||||||
subDec := &decoder{data: []byte(str)}
|
subDec := &decoder{data: []byte(str)}
|
||||||
return subDec.decodeValue(reflectValue, "")
|
return subDec.decodeValue(reflectValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -166,20 +109,7 @@ func (d *decoder) decodeValue(reflectValue reflect.Value, timeFormat string) err
|
|||||||
case reflect.String:
|
case reflect.String:
|
||||||
reflectValue.SetString(literal)
|
reflectValue.SetString(literal)
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
// 优先作为数字处理以保留精度 (int64)
|
reflectValue.Set(reflect.ValueOf(literal))
|
||||||
if literal == "true" {
|
|
||||||
reflectValue.Set(reflect.ValueOf(true))
|
|
||||||
} else if literal == "false" {
|
|
||||||
reflectValue.Set(reflect.ValueOf(false))
|
|
||||||
} else if literal == "null" {
|
|
||||||
reflectValue.Set(reflect.Zero(reflectValue.Type()))
|
|
||||||
} else if strings.Contains(literal, ".") || strings.Contains(literal, "e") || strings.Contains(literal, "E") {
|
|
||||||
reflectValue.Set(reflect.ValueOf(Float64(literal)))
|
|
||||||
} else if i, err := strconv.ParseInt(literal, 10, 64); err == nil {
|
|
||||||
reflectValue.Set(reflect.ValueOf(i))
|
|
||||||
} else {
|
|
||||||
reflectValue.Set(reflect.ValueOf(literal))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -203,9 +133,9 @@ func (d *decoder) decodeObject(reflectValue reflect.Value) error {
|
|||||||
if isMap && reflectValue.IsNil() {
|
if isMap && reflectValue.IsNil() {
|
||||||
reflectValue.Set(reflect.MakeMap(reflectValue.Type()))
|
reflectValue.Set(reflect.MakeMap(reflectValue.Type()))
|
||||||
}
|
}
|
||||||
var descriptor *decoderStructDescriptor
|
var fieldMap map[string]int
|
||||||
if isStruct {
|
if isStruct {
|
||||||
descriptor = getDecoderFieldMap(reflectValue.Type())
|
fieldMap = getDecoderFieldMap(reflectValue.Type())
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@ -222,16 +152,10 @@ func (d *decoder) decodeObject(reflectValue reflect.Value) error {
|
|||||||
|
|
||||||
if isStruct {
|
if isStruct {
|
||||||
// Frictionless 匹配
|
// Frictionless 匹配
|
||||||
fieldIndex, isTime, format, ok := matchField(key, descriptor)
|
fieldIndex, ok := matchField(key, fieldMap)
|
||||||
if ok {
|
if ok {
|
||||||
if isTime {
|
if err := d.decodeValue(reflectValue.Field(fieldIndex)); err != nil {
|
||||||
if err := d.decodeValue(reflectValue.Field(fieldIndex), format); err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := d.decodeValue(reflectValue.Field(fieldIndex), ""); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := d.skipValue(); err != nil {
|
if err := d.skipValue(); err != nil {
|
||||||
@ -246,7 +170,7 @@ func (d *decoder) decodeObject(reflectValue reflect.Value) error {
|
|||||||
keyValue.Set(reflect.ValueOf(key).Convert(keyType))
|
keyValue.Set(reflect.ValueOf(key).Convert(keyType))
|
||||||
|
|
||||||
valValue := reflect.New(valueType).Elem()
|
valValue := reflect.New(valueType).Elem()
|
||||||
if err := d.decodeValue(valValue, ""); err != nil {
|
if err := d.decodeValue(valValue); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
reflectValue.SetMapIndex(keyValue, valValue)
|
reflectValue.SetMapIndex(keyValue, valValue)
|
||||||
@ -287,7 +211,7 @@ func (d *decoder) decodeArray(reflectValue reflect.Value) error {
|
|||||||
isSlice := reflectValue.Kind() == reflect.Slice
|
isSlice := reflectValue.Kind() == reflect.Slice
|
||||||
for index := 0; ; index++ {
|
for index := 0; ; index++ {
|
||||||
if isSlice {
|
if isSlice {
|
||||||
if index >= reflectValue.Cap() {
|
if index >= reflectValue.Len() {
|
||||||
newCap := reflectValue.Cap() * 2
|
newCap := reflectValue.Cap() * 2
|
||||||
if newCap < 4 {
|
if newCap < 4 {
|
||||||
newCap = 4
|
newCap = 4
|
||||||
@ -296,11 +220,9 @@ func (d *decoder) decodeArray(reflectValue reflect.Value) error {
|
|||||||
reflect.Copy(newSlice, reflectValue)
|
reflect.Copy(newSlice, reflectValue)
|
||||||
reflectValue.Set(newSlice)
|
reflectValue.Set(newSlice)
|
||||||
} else {
|
} else {
|
||||||
if index >= reflectValue.Len() {
|
reflectValue.SetLen(index + 1)
|
||||||
reflectValue.SetLen(index + 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err := d.decodeValue(reflectValue.Index(index), ""); err != nil {
|
if err := d.decodeValue(reflectValue.Index(index)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -386,7 +308,7 @@ func (d *decoder) skipValue() error {
|
|||||||
d.pos++
|
d.pos++
|
||||||
if err := d.skipValue(); err != nil {
|
if err := d.skipValue(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.skipWhitespace()
|
d.skipWhitespace()
|
||||||
if d.data[d.pos] == ',' {
|
if d.data[d.pos] == ',' {
|
||||||
@ -421,92 +343,40 @@ func (d *decoder) skipValue() error {
|
|||||||
|
|
||||||
// Frictionless Logic
|
// Frictionless Logic
|
||||||
|
|
||||||
func getDecoderFieldMap(reflectType reflect.Type) *decoderStructDescriptor {
|
func getDecoderFieldMap(reflectType reflect.Type) map[string]int {
|
||||||
if val, ok := structFieldMapCache.Load(reflectType); ok {
|
if val, ok := structFieldMapCache.Load(reflectType); ok {
|
||||||
return val.(*decoderStructDescriptor)
|
return val.(map[string]int)
|
||||||
}
|
|
||||||
descriptor := &decoderStructDescriptor{
|
|
||||||
exactMatches: make(map[string]decoderFieldDescriptor),
|
|
||||||
}
|
}
|
||||||
|
m := make(map[string]int)
|
||||||
for index := 0; index < reflectType.NumField(); index++ {
|
for index := 0; index < reflectType.NumField(); index++ {
|
||||||
field := reflectType.Field(index)
|
field := reflectType.Field(index)
|
||||||
if !field.IsExported() {
|
if !field.IsExported() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldDesc := decoderFieldDescriptor{
|
|
||||||
index: index,
|
|
||||||
}
|
|
||||||
|
|
||||||
if field.Type == timeType || (field.Type.Kind() == reflect.Pointer && field.Type.Elem() == timeType) {
|
|
||||||
fieldDesc.isTime = true
|
|
||||||
fieldDesc.timeFormat = "2006-01-02 15:04:05.000"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Tag
|
// 1. Tag
|
||||||
tag := field.Tag.Get("json")
|
tag := field.Tag.Get("json")
|
||||||
if tag == "-" {
|
if tag != "" && tag != "-" {
|
||||||
continue
|
m[strings.Split(tag, ",")[0]] = index
|
||||||
}
|
}
|
||||||
if tag != "" {
|
|
||||||
parts := strings.Split(tag, ",")
|
|
||||||
tagName := parts[0]
|
|
||||||
for _, part := range parts {
|
|
||||||
if strings.HasPrefix(part, "format=") {
|
|
||||||
fieldDesc.timeFormat = strings.TrimPrefix(part, "format=")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tagName != "" {
|
|
||||||
descriptor.exactMatches[tagName] = fieldDesc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 原名
|
// 2. 原名
|
||||||
descriptor.exactMatches[field.Name] = fieldDesc
|
m[field.Name] = index
|
||||||
|
|
||||||
// 3. 归一化名
|
// 3. 归一化名
|
||||||
fieldDesc.normalized = normalizeKey(field.Name)
|
m[normalizeKey(field.Name)] = index
|
||||||
descriptor.fields = append(descriptor.fields, fieldDesc)
|
|
||||||
}
|
}
|
||||||
structFieldMapCache.Store(reflectType, descriptor)
|
structFieldMapCache.Store(reflectType, m)
|
||||||
return descriptor
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchField(key string, descriptor *decoderStructDescriptor) (int, bool, string, bool) {
|
func matchField(key string, fieldMap map[string]int) (int, bool) {
|
||||||
// 1. 精确匹配
|
// 1. 精确匹配
|
||||||
if f, ok := descriptor.exactMatches[key]; ok {
|
if index, ok := fieldMap[key]; ok {
|
||||||
return f.index, f.isTime, f.timeFormat, true
|
return index, true
|
||||||
}
|
}
|
||||||
// 2. 归一化匹配 (忽略大小写、下划线等) - 零分配比对
|
// 2. 归一化匹配 (忽略大小写、下划线等)
|
||||||
for _, f := range descriptor.fields {
|
if index, ok := fieldMap[normalizeKey(key)]; ok {
|
||||||
if normalizeEqual(key, f.normalized) {
|
return index, true
|
||||||
return f.index, f.isTime, f.timeFormat, true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return 0, false, "", false
|
return 0, false
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeEqual(raw string, normalized string) bool {
|
|
||||||
// normalized 已经是小写且只包含字母数字的字符串
|
|
||||||
// raw 是原始输入的 Key
|
|
||||||
j := 0
|
|
||||||
for _, r := range raw {
|
|
||||||
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if j >= len(normalized) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 比较 normalized 的下一个字符
|
|
||||||
// 由于 normalized 是由 normalizeKey 生成的,我们可以安全地假设它只包含字母数字且已转小写
|
|
||||||
// 为了完全正确且无分配地获取 normalized 的下一个 rune:
|
|
||||||
nr, size := utf8.DecodeRuneInString(normalized[j:])
|
|
||||||
if unicode.ToLower(r) != nr {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
j += size
|
|
||||||
}
|
|
||||||
return j == len(normalized)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeKey(str string) string {
|
func normalizeKey(str string) string {
|
||||||
|
|||||||
235
json_encoder.go
235
json_encoder.go
@ -4,36 +4,20 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var bufferPool = sync.Pool{
|
||||||
bufferPool = sync.Pool{
|
New: func() any {
|
||||||
New: func() any {
|
return new(bytes.Buffer)
|
||||||
return new(bytes.Buffer)
|
},
|
||||||
},
|
|
||||||
}
|
|
||||||
encoderStructCache sync.Map
|
|
||||||
)
|
|
||||||
|
|
||||||
type encoderFieldDescriptor struct {
|
|
||||||
index int
|
|
||||||
name string
|
|
||||||
isAnonymous bool
|
|
||||||
isTime bool
|
|
||||||
timeFormat string
|
|
||||||
keepKey bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type encoderStructDescriptor struct {
|
|
||||||
fields []encoderFieldDescriptor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type fastEncoder struct {
|
type fastEncoder struct {
|
||||||
buffer *bytes.Buffer
|
buffer *bytes.Buffer
|
||||||
desensitizeKeys map[string]bool
|
desensitizeKeys map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,12 +42,6 @@ func (encoder *fastEncoder) encodeValue(reflectValue reflect.Value, path string)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 time.Time
|
|
||||||
if reflectValue.Type() == timeType {
|
|
||||||
encoder.writeTime(reflectValue.Interface().(time.Time), "2006-01-02 15:04:05.000")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch reflectValue.Kind() {
|
switch reflectValue.Kind() {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
if reflectValue.Bool() {
|
if reflectValue.Bool() {
|
||||||
@ -97,15 +75,6 @@ func (encoder *fastEncoder) encodeValue(reflectValue reflect.Value, path string)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (encoder *fastEncoder) writeTime(t time.Time, format string) {
|
|
||||||
if format == "" {
|
|
||||||
format = "2006-01-02 15:04:05.000"
|
|
||||||
}
|
|
||||||
encoder.buffer.WriteByte('"')
|
|
||||||
encoder.buffer.WriteString(t.In(DefaultTimeZone.loc).Format(format))
|
|
||||||
encoder.buffer.WriteByte('"')
|
|
||||||
}
|
|
||||||
|
|
||||||
func (encoder *fastEncoder) encodeSlice(reflectValue reflect.Value, path string) error {
|
func (encoder *fastEncoder) encodeSlice(reflectValue reflect.Value, path string) error {
|
||||||
if reflectValue.IsNil() && reflectValue.Kind() == reflect.Slice {
|
if reflectValue.IsNil() && reflectValue.Kind() == reflect.Slice {
|
||||||
encoder.buffer.WriteString("[]")
|
encoder.buffer.WriteString("[]")
|
||||||
@ -165,27 +134,25 @@ func (encoder *fastEncoder) encodeMap(reflectValue reflect.Value, path string) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
encoder.buffer.WriteByte('{')
|
encoder.buffer.WriteByte('{')
|
||||||
iter := reflectValue.MapRange()
|
keys := reflectValue.MapKeys()
|
||||||
isFirst := true
|
// 为了输出稳定,对 Key 进行排序
|
||||||
for iter.Next() {
|
sort.Slice(keys, func(index1, index2 int) bool {
|
||||||
if !isFirst {
|
return String(keys[index1].Interface()) < String(keys[index2].Interface())
|
||||||
|
})
|
||||||
|
|
||||||
|
for index, key := range keys {
|
||||||
|
if index > 0 {
|
||||||
encoder.buffer.WriteByte(',')
|
encoder.buffer.WriteByte(',')
|
||||||
}
|
}
|
||||||
isFirst = false
|
|
||||||
|
|
||||||
key := iter.Key()
|
|
||||||
keyName := String(key.Interface())
|
keyName := String(key.Interface())
|
||||||
encoder.writeString(keyName)
|
encoder.writeString(keyName)
|
||||||
encoder.buffer.WriteByte(':')
|
encoder.buffer.WriteByte(':')
|
||||||
|
|
||||||
newPath := ""
|
newPath := keyName
|
||||||
if encoder.desensitizeKeys != nil {
|
if path != "" {
|
||||||
newPath = keyName
|
newPath = path + "." + keyName
|
||||||
if path != "" {
|
|
||||||
newPath = path + "." + keyName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err := encoder.encodeValue(iter.Value(), newPath); err != nil {
|
if err := encoder.encodeValue(reflectValue.MapIndex(key), newPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,143 +160,85 @@ func (encoder *fastEncoder) encodeMap(reflectValue reflect.Value, path string) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEncoderStructDescriptor(reflectType reflect.Type) *encoderStructDescriptor {
|
func (encoder *fastEncoder) encodeStruct(reflectValue reflect.Value, path string) error {
|
||||||
if val, ok := encoderStructCache.Load(reflectType); ok {
|
encoder.buffer.WriteByte('{')
|
||||||
return val.(*encoderStructDescriptor)
|
reflectType := reflectValue.Type()
|
||||||
}
|
first := true
|
||||||
|
|
||||||
descriptor := &encoderStructDescriptor{}
|
|
||||||
for index := 0; index < reflectType.NumField(); index++ {
|
for index := 0; index < reflectType.NumField(); index++ {
|
||||||
field := reflectType.Field(index)
|
field := reflectType.Field(index)
|
||||||
if !field.IsExported() {
|
if !field.IsExported() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldDesc := encoderFieldDescriptor{
|
// 处理匿名嵌入
|
||||||
index: index,
|
if field.Anonymous {
|
||||||
name: field.Name,
|
// 这里简单处理,实际上标准库会展开。我们为了保持算法一致性,直接递归。
|
||||||
isAnonymous: field.Anonymous,
|
// 但要注意 JSON Tag 可能会覆盖
|
||||||
}
|
|
||||||
|
|
||||||
if field.Type == timeType || (field.Type.Kind() == reflect.Pointer && field.Type.Elem() == timeType) {
|
|
||||||
fieldDesc.isTime = true
|
|
||||||
fieldDesc.timeFormat = "2006-01-02 15:04:05.000"
|
|
||||||
}
|
|
||||||
|
|
||||||
tag := field.Tag.Get("json")
|
|
||||||
if tag == "-" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fieldDesc.keepKey = strings.Contains(string(field.Tag), "keepKey")
|
|
||||||
|
|
||||||
if tag != "" {
|
if !first {
|
||||||
parts := strings.Split(tag, ",")
|
|
||||||
for _, part := range parts {
|
|
||||||
if strings.HasPrefix(part, "format=") {
|
|
||||||
fieldDesc.timeFormat = strings.TrimPrefix(part, "format=")
|
|
||||||
} else if fieldDesc.name == field.Name && part != "" { // 防止空 tag 抹掉字段名
|
|
||||||
fieldDesc.name = part
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tag == "" && !fieldDesc.keepKey && !field.Anonymous {
|
|
||||||
fieldDesc.name = GetLowerName(field.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptor.fields = append(descriptor.fields, fieldDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
encoderStructCache.Store(reflectType, descriptor)
|
|
||||||
return descriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (encoder *fastEncoder) encodeStruct(reflectValue reflect.Value, path string) error {
|
|
||||||
encoder.buffer.WriteByte('{')
|
|
||||||
first := true
|
|
||||||
err := encoder.encodeStructFields(reflectValue, path, &first)
|
|
||||||
encoder.buffer.WriteByte('}')
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (encoder *fastEncoder) encodeStructFields(reflectValue reflect.Value, path string, first *bool) error {
|
|
||||||
descriptor := getEncoderStructDescriptor(reflectValue.Type())
|
|
||||||
for _, fieldDesc := range descriptor.fields {
|
|
||||||
fieldValue := reflectValue.Field(fieldDesc.index)
|
|
||||||
|
|
||||||
if fieldDesc.isAnonymous {
|
|
||||||
fieldValue = RealValue(fieldValue)
|
|
||||||
if fieldValue.Kind() == reflect.Struct {
|
|
||||||
if err := encoder.encodeStructFields(fieldValue, path, first); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*first {
|
|
||||||
encoder.buffer.WriteByte(',')
|
encoder.buffer.WriteByte(',')
|
||||||
}
|
}
|
||||||
*first = false
|
first = false
|
||||||
|
|
||||||
encoder.writeString(fieldDesc.name)
|
// 算法转换 Key
|
||||||
|
keyName := field.Name
|
||||||
|
tag := field.Tag.Get("json")
|
||||||
|
keepKey := strings.Contains(string(field.Tag), "keepKey")
|
||||||
|
|
||||||
|
if tag != "" && tag != "-" {
|
||||||
|
parts := strings.Split(tag, ",")
|
||||||
|
keyName = parts[0]
|
||||||
|
} else if !keepKey {
|
||||||
|
// 执行首字母小写逻辑 (与 FixUpperCase 保持一致)
|
||||||
|
if len(keyName) > 0 && keyName[0] >= 'A' && keyName[0] <= 'Z' {
|
||||||
|
// 检查是否有小写字母,如果有则转小写 (UserID -> userID, ID -> ID)
|
||||||
|
hasLower := false
|
||||||
|
for charIndex := 0; charIndex < len(keyName); charIndex++ {
|
||||||
|
if keyName[charIndex] >= 'a' && keyName[charIndex] <= 'z' {
|
||||||
|
hasLower = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasLower {
|
||||||
|
keyName = strings.ToLower(keyName[:1]) + keyName[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder.writeString(keyName)
|
||||||
encoder.buffer.WriteByte(':')
|
encoder.buffer.WriteByte(':')
|
||||||
|
|
||||||
newPath := ""
|
newPath := keyName
|
||||||
if encoder.desensitizeKeys != nil {
|
if path != "" {
|
||||||
newPath = fieldDesc.name
|
newPath = path + "." + keyName
|
||||||
if path != "" {
|
|
||||||
newPath = path + "." + fieldDesc.name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if err := encoder.encodeValue(reflectValue.Field(index), newPath); err != nil {
|
||||||
if fieldDesc.isTime {
|
|
||||||
v := RealValue(fieldValue)
|
|
||||||
if v.IsValid() && v.Type() == timeType {
|
|
||||||
encoder.writeTime(v.Interface().(time.Time), fieldDesc.timeFormat)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := encoder.encodeValue(fieldValue, newPath); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
encoder.buffer.WriteByte('}')
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (encoder *fastEncoder) writeString(str string) {
|
func (encoder *fastEncoder) writeString(str string) {
|
||||||
encoder.buffer.WriteByte('"')
|
encoder.buffer.WriteByte('"')
|
||||||
start := 0
|
for index := 0; index < len(str); index++ {
|
||||||
for i := 0; i < len(str); i++ {
|
char := str[index]
|
||||||
char := str[i]
|
if char == '"' || char == '\\' {
|
||||||
if char < 0x20 || char == '"' || char == '\\' {
|
encoder.buffer.WriteByte('\\')
|
||||||
if start < i {
|
encoder.buffer.WriteByte(char)
|
||||||
encoder.buffer.WriteString(str[start:i])
|
} else if char == '\n' {
|
||||||
}
|
encoder.buffer.WriteString("\\n")
|
||||||
switch char {
|
} else if char == '\r' {
|
||||||
case '"':
|
encoder.buffer.WriteString("\\r")
|
||||||
encoder.buffer.WriteString(`\"`)
|
} else if char == '\t' {
|
||||||
case '\\':
|
encoder.buffer.WriteString("\\t")
|
||||||
encoder.buffer.WriteString(`\\`)
|
} else {
|
||||||
case '\n':
|
encoder.buffer.WriteByte(char)
|
||||||
encoder.buffer.WriteString(`\n`)
|
|
||||||
case '\r':
|
|
||||||
encoder.buffer.WriteString(`\r`)
|
|
||||||
case '\t':
|
|
||||||
encoder.buffer.WriteString(`\t`)
|
|
||||||
default:
|
|
||||||
// 其他不可见字符
|
|
||||||
encoder.buffer.WriteString(`\u00`)
|
|
||||||
encoder.buffer.WriteByte("0123456789abcdef"[char>>4])
|
|
||||||
encoder.buffer.WriteByte("0123456789abcdef"[char&0xf])
|
|
||||||
}
|
|
||||||
start = i + 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if start < len(str) {
|
|
||||||
encoder.buffer.WriteString(str[start:])
|
|
||||||
}
|
|
||||||
encoder.buffer.WriteByte('"')
|
encoder.buffer.WriteByte('"')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
119
merge_test.go
119
merge_test.go
@ -1,119 +0,0 @@
|
|||||||
package cast_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DBConfig struct {
|
|
||||||
Host string
|
|
||||||
Port int
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapStructMerge(t *testing.T) {
|
|
||||||
// Initial configuration with default values
|
|
||||||
dst := map[string]DBConfig{
|
|
||||||
"mysql": {Host: "localhost", Port: 3306},
|
|
||||||
}
|
|
||||||
|
|
||||||
// New data (e.g., from environment variables) that only overrides Host
|
|
||||||
src := map[string]any{
|
|
||||||
"mysql": map[string]any{
|
|
||||||
"Host": "127.0.0.1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform conversion/merge
|
|
||||||
cast.Convert(&dst, src)
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
mysql, ok := dst["mysql"]
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("mysql config not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if mysql.Host != "127.0.0.1" {
|
|
||||||
t.Errorf("Expected Host 127.0.0.1, got %s", mysql.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mysql.Port != 3306 {
|
|
||||||
t.Errorf("Expected Port 3306, got %d", mysql.Port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapStructPointerMerge(t *testing.T) {
|
|
||||||
// Initial configuration with default values (using pointers)
|
|
||||||
dst := map[string]*DBConfig{
|
|
||||||
"mysql": {Host: "localhost", Port: 3306},
|
|
||||||
}
|
|
||||||
|
|
||||||
// New data that only overrides Host
|
|
||||||
src := map[string]any{
|
|
||||||
"mysql": map[string]any{
|
|
||||||
"Host": "127.0.0.1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform conversion/merge
|
|
||||||
cast.Convert(&dst, src)
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
mysql, ok := dst["mysql"]
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("mysql config not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if mysql.Host != "127.0.0.1" {
|
|
||||||
t.Errorf("Expected Host 127.0.0.1, got %s", mysql.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mysql.Port != 3306 {
|
|
||||||
t.Errorf("Expected Port 3306, got %d", mysql.Port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSliceToMapMerge(t *testing.T) {
|
|
||||||
dst := map[string]int{
|
|
||||||
"a": 1,
|
|
||||||
"b": 2,
|
|
||||||
}
|
|
||||||
src := []any{"b", 20, "c", 30}
|
|
||||||
|
|
||||||
cast.Convert(&dst, src)
|
|
||||||
|
|
||||||
if dst["a"] != 1 {
|
|
||||||
t.Errorf("Expected a=1, got %d", dst["a"])
|
|
||||||
}
|
|
||||||
if dst["b"] != 20 {
|
|
||||||
t.Errorf("Expected b=20, got %d", dst["b"])
|
|
||||||
}
|
|
||||||
if dst["c"] != 30 {
|
|
||||||
t.Errorf("Expected c=30, got %d", dst["c"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStructToMapMerge(t *testing.T) {
|
|
||||||
type Config struct {
|
|
||||||
Host string
|
|
||||||
Port int
|
|
||||||
}
|
|
||||||
dst := map[string]any{
|
|
||||||
"host": "localhost",
|
|
||||||
"port": 3306,
|
|
||||||
"user": "root",
|
|
||||||
}
|
|
||||||
// Note: Struct fields are always "present", so Port:0 will overwrite port:3306
|
|
||||||
src := Config{Host: "127.0.0.1", Port: 8080}
|
|
||||||
|
|
||||||
cast.Convert(&dst, src)
|
|
||||||
|
|
||||||
if dst["host"] != "127.0.0.1" {
|
|
||||||
t.Errorf("Expected host 127.0.0.1, got %v", dst["host"])
|
|
||||||
}
|
|
||||||
if cast.Int(dst["port"]) != 8080 {
|
|
||||||
t.Errorf("Expected port 8080, got %v", dst["port"])
|
|
||||||
}
|
|
||||||
if dst["user"] != "root" {
|
|
||||||
t.Errorf("Expected user root, got %v", dst["user"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
package cast_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTo(t *testing.T) {
|
|
||||||
// Basic types
|
|
||||||
if v := cast.To[int]("123"); v != 123 {
|
|
||||||
t.Errorf("To[int] failed: %v", v)
|
|
||||||
}
|
|
||||||
if v := cast.To[int]("abc"); v != 0 {
|
|
||||||
t.Errorf("To[int] for invalid input should be 0, got %v", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slice
|
|
||||||
s := cast.To[[]int]([]string{"1", "2", "3"})
|
|
||||||
if len(s) != 3 || s[0] != 1 || s[1] != 2 || s[2] != 3 {
|
|
||||||
t.Errorf("To[[]int] failed: %v", s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map
|
|
||||||
m := cast.To[map[string]int](map[string]string{"a": "1", "b": "2"})
|
|
||||||
if len(m) != 2 || m["a"] != 1 || m["b"] != 2 {
|
|
||||||
t.Errorf("To[map[string]int] failed: %v", m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON Auto conversion (Struct to String)
|
|
||||||
type User struct {
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
u := User{Name: "Alice"}
|
|
||||||
js := cast.To[string](u)
|
|
||||||
if js != `{"name":"Alice"}` {
|
|
||||||
t.Errorf("To[string] for struct failed: %v", js)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON Auto conversion (String to Struct)
|
|
||||||
u2 := cast.To[User](`{"name":"Bob"}`)
|
|
||||||
if u2.Name != "Bob" {
|
|
||||||
t.Errorf("To[User] from string failed: %v", u2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAsWrapper(t *testing.T) {
|
|
||||||
// As
|
|
||||||
if v := cast.As(123, nil); v != 123 {
|
|
||||||
t.Errorf("As failed: %v", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapSliceAPIs(t *testing.T) {
|
|
||||||
// ToMap
|
|
||||||
m := cast.As(cast.ToMap[string, int]([]any{"a", "1", "b", 2}))
|
|
||||||
if m["a"] != 1 || m["b"] != 2 {
|
|
||||||
t.Errorf("ToMap failed: %v", m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToSlice
|
|
||||||
s := cast.As(cast.ToSlice[int]([]string{"10", "20"}))
|
|
||||||
if len(s) != 2 || s[0] != 10 || s[1] != 20 {
|
|
||||||
t.Errorf("ToSlice failed: %v", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
391
time.go
391
time.go
@ -1,391 +0,0 @@
|
|||||||
package cast
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TimeZone 定义了特定时区上下文下的时间操作
|
|
||||||
type TimeZone struct {
|
|
||||||
loc *time.Location
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTimeZone 创建一个时区上下文
|
|
||||||
func NewTimeZone(loc *time.Location) *TimeZone {
|
|
||||||
if loc == nil {
|
|
||||||
loc = time.Local
|
|
||||||
}
|
|
||||||
return &TimeZone{loc: loc}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultTimeZone 全局默认时区,默认为本地时区
|
|
||||||
var DefaultTimeZone = NewTimeZone(time.Local)
|
|
||||||
|
|
||||||
// SetDefaultTimeZone 修改全局默认时区
|
|
||||||
func SetDefaultTimeZone(loc *time.Location) {
|
|
||||||
if loc != nil {
|
|
||||||
DefaultTimeZone = NewTimeZone(loc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在包级别预先初始化 Replacer,0 额外分配
|
|
||||||
var timeFormatReplacer = strings.NewReplacer(
|
|
||||||
"YYYY", "2006", "YY", "06",
|
|
||||||
"MM", "01", "M", "1",
|
|
||||||
"DD", "02", "D", "2",
|
|
||||||
"HH", "15", "hh", "03", "h", "3",
|
|
||||||
"mm", "04", "ss", "05",
|
|
||||||
"a", "pm", "A", "PM",
|
|
||||||
"ZZ", "-0700", "Z", "-07:00",
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseTime 将任意类型转换为 time.Time。
|
|
||||||
// 支持:time.Time, 时间戳 (秒/毫秒/微秒/纳秒), RFC3339, JS 格式, 中文格式等。
|
|
||||||
// 转换失败返回零值 time.Time{} 以保持 cast 的静默风格。
|
|
||||||
func (tz *TimeZone) ParseTime(v any) time.Time {
|
|
||||||
if v == nil {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tm, ok := v.(time.Time); ok {
|
|
||||||
return tm.In(tz.loc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 优先探测是否为原生数字类型 (时间戳),彻底 0 分配
|
|
||||||
switch val := v.(type) {
|
|
||||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
|
||||||
num := Int64(val)
|
|
||||||
if num > 0 {
|
|
||||||
switch {
|
|
||||||
case num < 1e10: // 秒
|
|
||||||
return time.Unix(num, 0).In(tz.loc)
|
|
||||||
case num < 1e13: // 毫秒
|
|
||||||
return time.UnixMilli(num).In(tz.loc)
|
|
||||||
case num < 1e16: // 微秒
|
|
||||||
return time.UnixMicro(num).In(tz.loc)
|
|
||||||
default: // 纳秒
|
|
||||||
return time.Unix(0, num).In(tz.loc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 数字没命中,再老老实实当字符串处理
|
|
||||||
str := strings.TrimSpace(String(v))
|
|
||||||
if str == "" {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var tm time.Time
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// 3. 处理纯数字字符串 (紧凑格式或时间戳)
|
|
||||||
if num, err := strconv.ParseInt(str, 10, 64); err == nil && num > 0 {
|
|
||||||
// 紧凑格式解析
|
|
||||||
switch len(str) {
|
|
||||||
case 14: // 20060102150405
|
|
||||||
if tm, err = time.ParseInLocation("20060102150405", str, tz.loc); err == nil {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
case 8: // 20060102
|
|
||||||
if tm, err = time.ParseInLocation("20060102", str, tz.loc); err == nil {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
case 6: // 150405 或 060102
|
|
||||||
if tm, err = time.ParseInLocation("150405", str, tz.loc); err == nil {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
if tm, err = time.ParseInLocation("060102", str, tz.loc); err == nil {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 时间戳处理
|
|
||||||
switch {
|
|
||||||
case num < 1e10: // 秒
|
|
||||||
return time.Unix(num, 0).In(tz.loc)
|
|
||||||
case num < 1e13: // 毫秒
|
|
||||||
return time.UnixMilli(num).In(tz.loc)
|
|
||||||
case num < 1e16: // 微秒
|
|
||||||
return time.UnixMicro(num).In(tz.loc)
|
|
||||||
default: // 纳秒
|
|
||||||
return time.Unix(0, num).In(tz.loc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 标准格式与常见变体
|
|
||||||
// RFC3339 (带 T 和 Z 或偏移量)
|
|
||||||
if strings.Contains(str, "T") {
|
|
||||||
if tm, err = time.Parse(time.RFC3339Nano, str); err == nil {
|
|
||||||
return tm.In(tz.loc)
|
|
||||||
}
|
|
||||||
if tm, err = time.Parse(time.RFC3339, str); err == nil {
|
|
||||||
return tm.In(tz.loc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理分隔符:-, /, .
|
|
||||||
if len(str) >= 10 && (str[4] == '-' || str[4] == '/' || str[4] == '.') {
|
|
||||||
sep := str[4]
|
|
||||||
layout := fmt.Sprintf("2006%c01%c02", sep, sep)
|
|
||||||
|
|
||||||
// 完整日期时间
|
|
||||||
if len(str) >= 19 {
|
|
||||||
tsep := str[10] // 通常是 ' ' 或 'T'
|
|
||||||
if len(str) >= 20 && str[19] == '.' {
|
|
||||||
// 带纳秒
|
|
||||||
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05.999999999", tsep), str, tz.loc); err == nil {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05", tsep), str[:19], tz.loc); err == nil {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 纯日期
|
|
||||||
if tm, err = time.ParseInLocation(layout, str[:10], tz.loc); err == nil {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 06-01-02 这种短年份格式
|
|
||||||
if len(str) >= 8 && (str[2] == '-' || str[2] == '/' || str[2] == '.') {
|
|
||||||
sep := str[2]
|
|
||||||
layout := fmt.Sprintf("06%c01%c02", sep, sep)
|
|
||||||
if len(str) >= 17 && (str[8] == ' ' || str[8] == 'T') {
|
|
||||||
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05", str[8]), str[:17], tz.loc); err == nil {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tm, err = time.ParseInLocation(layout, str[:8], tz.loc); err == nil {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 特殊格式:JS, HTTP, 中文
|
|
||||||
if strings.Contains(str, "GMT") || strings.Contains(str, "CST") {
|
|
||||||
cleanStr := strings.SplitN(str, " (", 2)[0]
|
|
||||||
cleanStr = strings.Replace(cleanStr, "CST", "+0800", 1)
|
|
||||||
|
|
||||||
formats := []string{
|
|
||||||
"Mon Jan 02 2006 15:04:05 GMT-0700",
|
|
||||||
"Mon, 02 Jan 2006 15:04:05 GMT",
|
|
||||||
time.RFC1123Z,
|
|
||||||
time.RFC1123,
|
|
||||||
time.UnixDate,
|
|
||||||
}
|
|
||||||
for _, f := range formats {
|
|
||||||
if tm, err = time.Parse(f, cleanStr); err == nil {
|
|
||||||
return tm.In(tz.loc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 中文格式解析
|
|
||||||
if strings.ContainsAny(str, "年月日时点分秒") {
|
|
||||||
return tz.parseCN(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 纯时间格式
|
|
||||||
if strings.Contains(str, ":") {
|
|
||||||
if tm, err = time.ParseInLocation("15:04:05.999999", str, tz.loc); err == nil {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
if tm, err = time.ParseInLocation("15:04:05", str, tz.loc); err == nil {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
if tm, err = time.ParseInLocation("15:04", str, tz.loc); err == nil {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cnDateRegex = regexp.MustCompile(`(\d{2,4})?年?(\d{1,2})月(\d{1,2})日`)
|
|
||||||
var cnTimeRegex = regexp.MustCompile(`(上午|下午)?(\d{1,2})(?:时|点|:)(\d{1,2})(?:分|:)(\d{1,2})?秒?`)
|
|
||||||
|
|
||||||
func (tz *TimeZone) parseCN(str string) time.Time {
|
|
||||||
str = strings.ReplaceAll(str, ":", ":")
|
|
||||||
str = strings.ReplaceAll(str, " ", "")
|
|
||||||
var y, m, d, h, mm, s int
|
|
||||||
if matches := cnDateRegex.FindStringSubmatch(str); len(matches) == 4 {
|
|
||||||
y = Int(matches[1])
|
|
||||||
m = Int(matches[2])
|
|
||||||
d = Int(matches[3])
|
|
||||||
if y > 0 && y < 100 {
|
|
||||||
y += 2000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if matches := cnTimeRegex.FindStringSubmatch(str); len(matches) == 5 {
|
|
||||||
h = Int(matches[2])
|
|
||||||
mm = Int(matches[3])
|
|
||||||
s = Int(matches[4])
|
|
||||||
if matches[1] == "下午" && h < 12 {
|
|
||||||
h += 12
|
|
||||||
}
|
|
||||||
if matches[1] == "上午" && h >= 12 {
|
|
||||||
h -= 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
now := time.Now().In(tz.loc)
|
|
||||||
if y == 0 {
|
|
||||||
y = now.Year()
|
|
||||||
}
|
|
||||||
if m == 0 {
|
|
||||||
m = int(now.Month())
|
|
||||||
}
|
|
||||||
if d == 0 {
|
|
||||||
d = now.Day()
|
|
||||||
}
|
|
||||||
return time.Date(y, time.Month(m), d, h, mm, s, 0, tz.loc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseTime 将任意类型转换为 time.Time。
|
|
||||||
func ParseTime(v any) time.Time {
|
|
||||||
return DefaultTimeZone.ParseTime(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatTime 格式化时间。
|
|
||||||
// layout 支持: YYYY-MM-DD HH:mm:ss, YYYY/MM/DD, HH:mm 等直观格式。
|
|
||||||
func (tz *TimeZone) FormatTime(layout string, v any) string {
|
|
||||||
tm := tz.ParseTime(v)
|
|
||||||
if tm.IsZero() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
// 一次性流式替换,性能直接翻倍
|
|
||||||
return tm.Format(timeFormatReplacer.Replace(layout))
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatTime 格式化时间。
|
|
||||||
func FormatTime(layout string, v any) string {
|
|
||||||
return DefaultTimeZone.FormatTime(layout, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTime 时间加减 DSL。
|
|
||||||
// 格式如: "+1Y-2M+3D", "+1h30m", "-1s"。
|
|
||||||
// 单位支持: Y (年), M (月), D (天), h, m, s, ms, us, ns。
|
|
||||||
func (tz *TimeZone) AddTime(expr string, v any) time.Time {
|
|
||||||
tm := tz.ParseTime(v)
|
|
||||||
if tm.IsZero() {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
if expr == "" {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
years, months, days := 0, 0, 0
|
|
||||||
var duration time.Duration
|
|
||||||
|
|
||||||
for i < len(expr) {
|
|
||||||
sign := 1
|
|
||||||
if expr[i] == '+' {
|
|
||||||
i++
|
|
||||||
} else if expr[i] == '-' {
|
|
||||||
sign = -1
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
j := i
|
|
||||||
for j < len(expr) && expr[j] >= '0' && expr[j] <= '9' {
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
num := 1
|
|
||||||
if j > i {
|
|
||||||
num, _ = strconv.Atoi(expr[i:j])
|
|
||||||
}
|
|
||||||
val := num * sign
|
|
||||||
|
|
||||||
i = j
|
|
||||||
unit := ""
|
|
||||||
// 匹配双字符单位 (ms, us, ns)
|
|
||||||
if i+2 <= len(expr) {
|
|
||||||
u2 := expr[i : i+2]
|
|
||||||
if u2 == "ms" || u2 == "us" || u2 == "ns" {
|
|
||||||
unit = u2
|
|
||||||
i += 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 匹配单字符单位
|
|
||||||
if unit == "" && i < len(expr) {
|
|
||||||
unit = expr[i : i+1]
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
switch unit {
|
|
||||||
case "Y":
|
|
||||||
years += val
|
|
||||||
case "M":
|
|
||||||
months += val
|
|
||||||
case "D":
|
|
||||||
days += val
|
|
||||||
case "h":
|
|
||||||
duration += time.Duration(val) * time.Hour
|
|
||||||
case "m":
|
|
||||||
duration += time.Duration(val) * time.Minute
|
|
||||||
case "s":
|
|
||||||
duration += time.Duration(val) * time.Second
|
|
||||||
case "ms":
|
|
||||||
duration += time.Duration(val) * time.Millisecond
|
|
||||||
case "us":
|
|
||||||
duration += time.Duration(val) * time.Microsecond
|
|
||||||
case "ns":
|
|
||||||
duration += time.Duration(val) * time.Nanosecond
|
|
||||||
default:
|
|
||||||
// 默认秒
|
|
||||||
duration += time.Duration(val) * time.Second
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if years != 0 || months != 0 || days != 0 {
|
|
||||||
tm = tm.AddDate(years, months, days)
|
|
||||||
}
|
|
||||||
return tm.Add(duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTime 时间加减 DSL。
|
|
||||||
func AddTime(expr string, v any) time.Time {
|
|
||||||
return DefaultTimeZone.AddTime(expr, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now 获取当前时间
|
|
||||||
func (tz *TimeZone) Now() time.Time { return time.Now().In(tz.loc) }
|
|
||||||
|
|
||||||
// Location 获取当前时区
|
|
||||||
func (tz *TimeZone) Location() *time.Location { return tz.loc }
|
|
||||||
|
|
||||||
// Now 获取当前时间
|
|
||||||
func Now() time.Time { return DefaultTimeZone.Now() }
|
|
||||||
|
|
||||||
// DescribeDuration 将时长转化为自然语言描述,例如 "1h 1m 1s"
|
|
||||||
func (tz *TimeZone) DescribeDuration(d time.Duration) string {
|
|
||||||
h := d / time.Hour
|
|
||||||
d -= h * time.Hour
|
|
||||||
m := d / time.Minute
|
|
||||||
d -= m * time.Minute
|
|
||||||
s := d / time.Second
|
|
||||||
|
|
||||||
var parts []string
|
|
||||||
if h > 0 {
|
|
||||||
parts = append(parts, fmt.Sprintf("%dh", h))
|
|
||||||
}
|
|
||||||
if m > 0 {
|
|
||||||
parts = append(parts, fmt.Sprintf("%dm", m))
|
|
||||||
}
|
|
||||||
if s > 0 {
|
|
||||||
parts = append(parts, fmt.Sprintf("%ds", s))
|
|
||||||
}
|
|
||||||
if len(parts) == 0 {
|
|
||||||
return "0s"
|
|
||||||
}
|
|
||||||
return strings.Join(parts, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// DescribeDuration 将时长转化为自然语言描述
|
|
||||||
func DescribeDuration(d time.Duration) string {
|
|
||||||
return DefaultTimeZone.DescribeDuration(d)
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
package cast_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
)
|
|
||||||
|
|
||||||
func BenchmarkParseTime_RFC3339(b *testing.B) {
|
|
||||||
s := "2023-05-04T12:34:56Z"
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = cast.ParseTime(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkParseTime_Standard(b *testing.B) {
|
|
||||||
s := "2023-05-04 12:34:56"
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = cast.ParseTime(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkParseTime_Numeric(b *testing.B) {
|
|
||||||
s := "20230504123456"
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = cast.ParseTime(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkToJSON_WithTime(b *testing.B) {
|
|
||||||
type User struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
u := User{ID: 1, Name: "Benchmark User", CreatedAt: time.Now()}
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _ = cast.ToJSON(u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
68
time_test.go
68
time_test.go
@ -1,68 +0,0 @@
|
|||||||
package cast_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseTime(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input any
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{"2023-05-04 12:34:56", "2023-05-04 12:34:56"},
|
|
||||||
{"2023/05/04 12:34:56", "2023-05-04 12:34:56"},
|
|
||||||
{"2023.05.04 12:34:56", "2023-05-04 12:34:56"},
|
|
||||||
{"20230504123456", "2023-05-04 12:34:56"},
|
|
||||||
{1683196496, "2023-05-04 10:34:56"}, // Unix timestamp (Example)
|
|
||||||
{"2023年5月4日 12时34分56秒", "2023-05-04 12:34:56"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set UTC for test stability if needed, or just compare formatted strings
|
|
||||||
loc, _ := time.LoadLocation("UTC")
|
|
||||||
cast.SetDefaultTimeZone(loc)
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
tm := cast.ParseTime(tt.input)
|
|
||||||
// We use a relative check for timestamp since it depends on the timezone if not careful
|
|
||||||
if tt.input == 1683196496 {
|
|
||||||
// 1683196496 is 2023-05-04 10:34:56 UTC
|
|
||||||
if tm.Format("2006-01-02 15:04:05") != tt.expected {
|
|
||||||
t.Errorf("ParseTime(%v) = %v, want %v", tt.input, tm.Format("2006-01-02 15:04:05"), tt.expected)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if tm.Format("2006-01-02 15:04:05") != tt.expected {
|
|
||||||
t.Errorf("ParseTime(%v) = %v, want %v", tt.input, tm.Format("2006-01-02 15:04:05"), tt.expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFormatTime(t *testing.T) {
|
|
||||||
tm := time.Date(2023, 5, 4, 12, 34, 56, 0, time.UTC)
|
|
||||||
formatted := cast.FormatTime("YYYY-MM-DD HH:mm:ss", tm)
|
|
||||||
if formatted != "2023-05-04 12:34:56" {
|
|
||||||
t.Errorf("FormatTime failed, got %s", formatted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddTime(t *testing.T) {
|
|
||||||
tm := time.Date(2023, 5, 4, 12, 34, 56, 0, time.UTC)
|
|
||||||
added := cast.AddTime("+1Y+1M+1D", tm)
|
|
||||||
expected := time.Date(2024, 6, 5, 12, 34, 56, 0, time.UTC)
|
|
||||||
if !added.Equal(expected) {
|
|
||||||
t.Errorf("AddTime failed, got %v, want %v", added, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTimeZoneNow(t *testing.T) {
|
|
||||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
|
||||||
tz := cast.NewTimeZone(loc)
|
|
||||||
now := tz.Now()
|
|
||||||
if now.Location().String() != "Asia/Shanghai" {
|
|
||||||
t.Errorf("TimeZone.Now() failed to set location, got %s", now.Location().String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user