diff --git a/CHANGELOG.md b/CHANGELOG.md index b3d5c09..5bae7a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,17 +3,11 @@ ## [v1.2.0] - 2026-05-04 ### Added - **语义化 API**: 引入 `To[T]` (严格/含错) 和 `As[T]` (静默/零值) 泛型接口,作为全能类型转换入口。 +- **摩擦消除工具**: 新增 `Quiet[T]` 和 `Must[T]` 通用辅助函数,用于包装 error-returning 函数。 - **智能穿透**: `To[T]` 支持 JSON 自动双向转换(Input string/[]byte <-> Target struct/map/slice)。 -- **容器 API 统一**: - - `ToMap` / `AsMap` (替代 `MakeMap`) - - `ToSlice` / `AsSlice` (替代 `MakeSlice`) - - `AsJSON` / `AsJSONBytes` (替代 `MustToJSON`) - - `AsFromJSON` (替代 `MustFromJSON`) ### Removed -- **YAML 支持**: 移除 `gopkg.in/yaml.v3` 依赖及所有 YAML 相关 API,专注 JSON 以保持核心极致精简。 -### Deprecated -- **MustXxx 系列**: 建议迁移至语义更明确的 `AsXxx`。 -- **MakeXxx 系列**: 建议迁移至 `AsMap` / `AsSlice`。 +- **YAML 支持**: 移除 `gopkg.in/yaml.v3` 依赖及所有 YAML 相关 API。 +- **API 整合**: 移除专门的 `AsXxx` 和 `MustXxx` 系列函数(如 `AsMap`, `AsJSON`, `MustToJSON` 等),推荐使用 `As[T]` 或 `Quiet/Must` 包装。 ## [v1.1.2] - 2026-05-04 ### Added diff --git a/README.md b/README.md index d52be01..aac8914 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ `@go/cast` 是一个为“敏捷开发”设计的 Go 基础工具库。其核心目标是**消除类型摩擦**,让开发者在处理数据时更关注“我想要什么”而不是“原本是什么”。 -* **语义化 API**:通过 `To[T]` (严格/含错) 和 `As[T]` (静默/零值) 提供一致的调用体验。 +* **语义化 API**:通过 `To[T]` (严格/含错) 配合 `Quiet` (静默) 或 `Must` (断言) 提供一致的调用体验。 * **智能穿透**:自动识别 JSON 文本、结构体映射、KV 展开等复杂场景。 * **极致性能**:内置 FastEncoder/FastDecoder,单路径处理,最小化内存分配。 @@ -22,23 +22,26 @@ go get apigo.cc/go/cast ```go import "apigo.cc/go/cast" -// 1. 语义化转换 (To[T] 返回错误, As[T] 返回零值) -age := cast.As[int]("18") // 18 -val, err := cast.To[int]("abc") // 0, error +// 1. 语义化转换 (To[T] 返回错误) +val, err := cast.To[int]("123") // 123, nil -// 2. 智能 JSON 自动转换 +// 2. 摩擦消除工具 (Quiet 返回零值, Must 失败 Panic) +age := cast.Quiet(cast.To[int]("abc")) // 0 +uid := cast.Must(cast.To[int]("100")) // 100 + +// 3. 快捷方式 (cast.As[T] 等价于 cast.Quiet(cast.To[T])) +name := cast.As[string](123) // "123" + +// 4. 智能 JSON 自动转换 // 输入 map/struct -> 目标 string: 自动序列化 js := cast.As[string](map[string]int{"age": 18}) // `{"age":18}` // 输入 string/[]byte -> 目标 struct/map: 自动反序列化 user, _ := cast.To[User](`{"name":"Tom"}`) -// 3. 复杂容器转换 (Map/Slice) +// 5. 复杂容器转换 (Map/Slice) m, _ := cast.ToMap[string, int]([]any{"id", "1", "score", 100}) -list := cast.AsSlice[int]([]string{"1", "2", "3"}) - -// 4. 泛型三元运算 -status := cast.If(isAdmin, "Admin", "User") +list := cast.As[[]int]([]string{"1", "2", "3"}) ``` ## 🛠 API 指南 @@ -47,21 +50,23 @@ status := cast.If(isAdmin, "Admin", "User") 1. **通用转换** * `To[T any](any) (T, error)` —— 万能转换入口。支持基础类型、Slice、Map 以及 JSON 的双向自动转换。 - * `As[T any](any) T` —— 零摩擦转换。失败时返回类型零值。 + * `As[T any](any) T` —— 零摩擦转换快捷方式。失败时返回类型零值。 -2. **Map / Slice 转换** +2. **错误处理工具** + * `Quiet[T any](T, error) T` —— 忽略错误,返回转换后的值(若有错则返回零值)。 + * `Must[T any](T, error) T` —— 严谨模式。若有错误则直接 Panic,常用于全局初始化。 + +3. **容器转换** * `ToMap[K, V](any) (map[K]V, error)` —— 构建新 Map。 - * `AsMap[K, V](any) map[K]V` —— 构建新 Map (失败返回空 Map)。 * `ToSlice[T](any) ([]T, error)` —— 构建新 Slice。 - * `AsSlice[T](any) []T` —— 构建新 Slice (失败返回空 Slice)。 * `FillMap(target, source)` | `FillSlice(target, source)` —— 填充现有容器。 -3. **JSON 序列化与构建** - * **编码**: `ToJSON(any) (string, error)` | `AsJSON(any) string` - * **解码**: `FromJSON[T](any) (T, error)` | `AsFromJSON[T](any) T` +4. **JSON 序列化与构建** + * **编码**: `ToJSON(any) (string, error)` | `ToJSONBytes(any) ([]byte, error)` + * **解码**: `UnmarshalJSON(data, target) error` | `FromJSON[T](any) (T, error)` * **进阶**: `ToJSONDesensitize(any, []string) (string, error)` (支持字段脱敏) -4. **泛型工具** +5. **泛型工具** * `If[T any](bool, T, T) T` —— 三元逻辑。 * `In[T comparable]([]T, T) bool` —— 包含判断。 * `Ptr[T any](T) *T` —— 快速取指针。 diff --git a/bench_test.go b/bench_test.go index c6a30bf..5e4c41d 100644 --- a/bench_test.go +++ b/bench_test.go @@ -71,7 +71,7 @@ func BenchmarkJSON(b *testing.B) { } u := User{ID: 1, Name: "Benchmark User"} for i := 0; i < b.N; i++ { - _ = cast.MustToJSON(u) + _ = cast.As[string](u) } } @@ -147,7 +147,7 @@ func BenchmarkToJSON_SimpleStruct(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { // 这里包含了你特有的 makeJSONType 清洗逻辑和 FixUpperCase 逻辑 - _ = cast.MustToJSON(u) + _ = cast.As[string](u) } } @@ -160,6 +160,6 @@ func BenchmarkToJSON_DirtyMap(b *testing.B) { } b.ResetTimer() for i := 0; i < b.N; i++ { - _ = cast.MustToJSON(val) + _ = cast.As[string](val) } } diff --git a/cast.go b/cast.go index 0e7b436..c6f4150 100644 --- a/cast.go +++ b/cast.go @@ -25,6 +25,23 @@ func In[T comparable](arr []T, val T) bool { return slices.Contains(arr, val) } +// Quiet 忽略错误,返回零值 +func Quiet[T any](v T, err error) T { + if err != nil { + var zero T + return zero + } + return v +} + +// Must 严谨模式,报错即刻崩溃 +func Must[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} + func RealValue(v reflect.Value) reflect.Value { for v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface { if v.IsNil() { @@ -70,17 +87,112 @@ func To[T any](v any) (T, error) { } // 4. 处理基础类型 - res := reflectCast(v, targetType) - if !res.IsValid() { - return zero, fmt.Errorf("cast to %v failed", targetType) + res, err := reflectCastE(v, targetType) + if err != nil { + return zero, err } return res.Interface().(T), nil } +func reflectCastE(value any, t reflect.Type) (reflect.Value, error) { + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := ToInt64E(value) + if err != nil { + return reflect.Value{}, err + } + return reflect.ValueOf(i).Convert(t), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + i, err := ToUint64E(value) + if err != nil { + return reflect.Value{}, err + } + return reflect.ValueOf(i).Convert(t), nil + case reflect.Float32, reflect.Float64: + f, err := ToFloat64E(value) + if err != nil { + return reflect.Value{}, err + } + return reflect.ValueOf(f).Convert(t), nil + case reflect.String: + return reflect.ValueOf(String(value)).Convert(t), nil + case reflect.Bool: + return reflect.ValueOf(Bool(value)).Convert(t), nil + case reflect.Interface: + if value == nil { + return reflect.Zero(t), nil + } + return reflect.ValueOf(value), nil + } + + if t == reflect.TypeOf(time.Duration(0)) { + return reflect.ValueOf(Duration(value)), nil + } + + return reflect.Zero(t), fmt.Errorf("unsupported target type: %v", t) +} + +func ToInt64E(v any) (int64, error) { + if i, ok := toInt64(v); ok { + return i, nil + } + s := String(v) + i, err := strconv.ParseInt(s, 10, 64) + if err == nil { + return i, nil + } + if f, err := strconv.ParseFloat(s, 64); err == nil { + return int64(f), nil + } + return 0, fmt.Errorf("cannot cast %v to int64", v) +} + +func ToUint64E(v any) (uint64, error) { + switch realValue := v.(type) { + case int, int8, int16, int32, int64: + return uint64(Int64(realValue)), nil + case uint, uint8, uint16, uint32, uint64: + return Uint64(realValue), nil + case float32, float64: + return uint64(Float64(realValue)), nil + case bool: + return If(realValue, uint64(1), uint64(0)), nil + } + s := String(v) + i, err := strconv.ParseUint(s, 10, 64) + if err == nil { + return i, nil + } + if f, err := strconv.ParseFloat(s, 64); err == nil { + return uint64(f), nil + } + return 0, fmt.Errorf("cannot cast %v to uint64", v) +} + +func ToFloat64E(v any) (float64, error) { + switch realValue := v.(type) { + case int, int8, int16, int32, int64: + return float64(Int64(realValue)), nil + case uint, uint8, uint16, uint32, uint64: + return float64(Uint64(realValue)), nil + case float32: + return float64(realValue), nil + case float64: + return realValue, nil + case bool: + return If(realValue, 1.0, 0.0), nil + } + s := String(v) + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, fmt.Errorf("cannot cast %v to float64", v) + } + return f, nil +} + // As 泛型转换 (失败返回零值) func As[T any](v any) T { - res, _ := To[T](v) - return res + return Quiet(To[T](v)) } func isJSONText(v any) bool { @@ -106,11 +218,11 @@ func isComplexType(t reflect.Type) bool { func isComplexValue(v any) bool { if v == nil { - return false + return true } rv := RealValue(reflect.ValueOf(v)) - if !rv.IsValid() { - return false + 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) @@ -336,21 +448,6 @@ func Duration(value any) time.Duration { return 0 } -func AsJSONBytes(value any) []byte { - j, _ := ToJSONBytes(value) - return j -} - -func AsJSON(value any) string { - s, _ := ToJSON(value) - return s -} - -func AsFromJSON[T any](data any) T { - v, _ := FromJSON[T](data) - return v -} - func ToJSONBytes(value any) ([]byte, error) { return fastToJSONBytes(value) } @@ -367,8 +464,6 @@ func ToJSONDesensitizeBytes(value any, keys []string) ([]byte, error) { return fastToJSONBytes(value, keys...) } -func MustToJSONBytes(value any) []byte { return AsJSONBytes(value) } - func ToJSON(value any) (string, error) { j, err := ToJSONBytes(value) if err != nil { @@ -377,10 +472,8 @@ func ToJSON(value any) (string, error) { return string(j), nil } -func MustToJSON(value any) string { return AsJSON(value) } - func PrettyToJSONBytes(value any) []byte { - j := AsJSONBytes(value) + j := Quiet(ToJSONBytes(value)) r := &bytes.Buffer{} if err := json.Indent(r, j, "", " "); err == nil { return r.Bytes() @@ -417,8 +510,6 @@ func FromJSON[T any](data any) (T, error) { return v, err } -func MustFromJSON[T any](data any) T { return AsFromJSON[T](data) } - // --- Others (Keep logic but clean style) --- func Split(s, sep string) []string { @@ -523,12 +614,6 @@ func ToMap[K comparable, V any](source any) (map[K]V, error) { return m, nil } -// AsMap 泛型构建新 Map (失败返回空 Map) -func AsMap[K comparable, V any](source any) map[K]V { - m, _ := ToMap[K, V](source) - return m -} - // ToSlice 泛型构建新 Slice func ToSlice[T any](source any) ([]T, error) { var s []T @@ -536,12 +621,6 @@ func ToSlice[T any](source any) ([]T, error) { return s, nil } -// AsSlice 泛型构建新 Slice (失败返回空 Slice) -func AsSlice[T any](source any) []T { - s, _ := ToSlice[T](source) - return s -} - // FillMap 将 source 填充到目标 map 中 (兼容旧 API 逻辑) func FillMap(target any, source any) { rv := reflect.ValueOf(target) @@ -647,40 +726,8 @@ func fillToSlice(sliceRv reflect.Value, source any) { } } -// MakeMap 泛型构建新 Map (DEPRECATED: Use AsMap) -func MakeMap[K comparable, V any](source any) map[K]V { - return AsMap[K, V](source) -} - -// MakeSlice 泛型构建新 Slice (DEPRECATED: Use AsSlice) -func MakeSlice[T any](source any) []T { - return AsSlice[T](source) -} - func reflectCast(value any, t reflect.Type) reflect.Value { - switch t.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return reflect.ValueOf(Int64(value)).Convert(t) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return reflect.ValueOf(Uint64(value)).Convert(t) - case reflect.Float32, reflect.Float64: - return reflect.ValueOf(Float64(value)).Convert(t) - case reflect.String: - return reflect.ValueOf(String(value)).Convert(t) - case reflect.Bool: - return reflect.ValueOf(Bool(value)).Convert(t) - case reflect.Interface: - if value == nil { - return reflect.Zero(t) - } - return reflect.ValueOf(value) - } - - if t == reflect.TypeOf(time.Duration(0)) { - return reflect.ValueOf(Duration(value)) - } - - return reflect.Zero(t) + return Quiet(reflectCastE(value, t)) } // 补充缺失的 Key 转换工具 diff --git a/cast_test.go b/cast_test.go index 157b649..2c9bbfd 100644 --- a/cast_test.go +++ b/cast_test.go @@ -32,7 +32,7 @@ func TestStructToJSON(t *testing.T) { Age int } u := User{Name: "Tom", Age: 18} - js := cast.MustToJSON(u) + js := cast.As[string](u) // 验证首字母小写逻辑 if !strings.Contains(js, `"name":"Tom"`) || !strings.Contains(js, `"age":18`) { t.Errorf("Struct to JSON auto-lowercase failed: %s", js) @@ -61,7 +61,7 @@ func TestSpecialJSON(t *testing.T) { // 标准 json.Marshal 会变成 " \u0026 " // 我们期望输出原始字符 " & " - js := cast.MustToJSON(c) + js := cast.As[string](c) expected := `{"text":" & "}` if js != expected { t.Errorf("Special JSON failed.\nExpected: %s\nActual: %s", expected, js) @@ -84,7 +84,7 @@ func TestComplexJSONType(t *testing.T) { }, } - js := cast.MustToJSON(data) + js := cast.As[string](data) // 期望 123 被转为 "123" 且内容正确 if !strings.Contains(js, `"123":"numeric key"`) || !strings.Contains(js, `"ok":true`) { t.Errorf("Complex JSON type failed: %s", js) @@ -138,7 +138,7 @@ func TestGojaMapToArrayFallback(t *testing.T) { "2": "cherry", // 字符串形式的数字也应该被正确转换 } - js := cast.MustToJSON(mockGojaArray) + js := cast.As[string](mockGojaArray) // 我们期望它被正确识别并转化为标准的 JSON 数组,且顺序不会乱 expected := `["apple","banana","cherry"]` @@ -194,7 +194,7 @@ func TestUnaddressableStruct(t *testing.T) { } }() - res := cast.MustToJSON(d) + res := cast.As[string](d) if !strings.Contains(res, "\"name\"") { t.Errorf("Value struct ToJSON failed to lowercase key: %s", res) } @@ -203,19 +203,19 @@ func TestUnaddressableStruct(t *testing.T) { func TestToJSON_Nil(t *testing.T) { // Nil slice should be [] var s []int - if js := cast.MustToJSON(s); js != "[]" { + if js := cast.As[string](s); js != "[]" { t.Errorf("Nil slice expected [], got %s", js) } // Nil map should be {} var m map[string]int - if js := cast.MustToJSON(m); js != "{}" { + if js := cast.As[string](m); js != "{}" { t.Errorf("Nil map expected {}, got %s", js) } // Nil pointer should be null var p *int - if js := cast.MustToJSON(p); js != "null" { + if js := cast.As[string](p); js != "null" { t.Errorf("Nil pointer expected null, got %s", js) } } @@ -247,7 +247,7 @@ func TestFastEncoder_MapAny(t *testing.T) { "userName": "admin", 123: "val", } - js := cast.MustToJSON(data) + js := cast.As[string](data) if !strings.Contains(js, `"123":"val"`) || !strings.Contains(js, `"userName":"admin"`) { t.Errorf("MapAny encoding failed: %s", js) } @@ -258,7 +258,7 @@ func TestFastEncoder_GojaArray(t *testing.T) { 0: "a", 1: "b", } - js := cast.MustToJSON(data) + js := cast.As[string](data) if js != `["a","b"]` { t.Errorf("Goja array fallback failed: %s", js) } diff --git a/conversion_test.go b/conversion_test.go index 1e14774..59da273 100644 --- a/conversion_test.go +++ b/conversion_test.go @@ -23,7 +23,7 @@ func TestToMap(t *testing.T) { } // Test slice-to-map (KV) - m2 := cast.AsMap[int, string]([]any{"1", "a", 2, 200}) + m2 := cast.As[map[int]string]([]any{"1", "a", 2, 200}) if m2[1] != "a" || m2[2] != "200" { t.Errorf("Slice-to-map failed: %v", m2) } @@ -63,22 +63,22 @@ func TestJSON(t *testing.T) { t.Errorf("FromJSON failed: %v, %v", err, c2) } - // Test AsFromJSON - c3 := cast.AsFromJSON[Config](data) + // Test As[T] from JSON + c3 := cast.As[Config](data) if c3.Port != 8080 { - t.Errorf("AsFromJSON failed: %v", c3) + t.Errorf("As[Config] failed: %v", c3) } } -func TestMakeSlice(t *testing.T) { - // Test MakeSlice with various inputs - s1 := cast.AsSlice[int]("123") +func TestGenericAsSlice(t *testing.T) { + // Test As with various inputs + s1 := cast.As[[]int]("123") if len(s1) != 1 || s1[0] != 123 { - t.Errorf("AsSlice primitive failed: %v", s1) + t.Errorf("As[[]int] primitive failed: %v", s1) } - s2 := cast.AsSlice[string]([]int{1, 2}) + s2 := cast.As[[]string]([]int{1, 2}) if len(s2) != 2 || s2[0] != "1" || s2[1] != "2" { - t.Errorf("AsSlice slice failed: %v", s2) + t.Errorf("As[[]string] slice failed: %v", s2) } } diff --git a/semantic_test.go b/semantic_test.go index 52b9b3b..6e0857f 100644 --- a/semantic_test.go +++ b/semantic_test.go @@ -46,6 +46,24 @@ func TestToAs(t *testing.T) { } } +func TestQuietMust(t *testing.T) { + // Quiet + if v := cast.Quiet(cast.To[int]("abc")); v != 0 { + t.Errorf("Quiet failed: %v", v) + } + if v := cast.Quiet(cast.To[int]("123")); v != 123 { + t.Errorf("Quiet failed: %v", v) + } + + // Must + defer func() { + if r := recover(); r == nil { + t.Errorf("Must should panic on error") + } + }() + cast.Must(cast.To[int]("abc")) +} + func TestMapSliceAPIs(t *testing.T) { // ToMap m, err := cast.ToMap[string, int]([]any{"a", "1", "b", 2}) @@ -53,10 +71,10 @@ func TestMapSliceAPIs(t *testing.T) { t.Errorf("ToMap failed: %v, %v", m, err) } - // AsMap - m2 := cast.AsMap[string, int]([]any{"c", "3"}) + // As[map...] + m2 := cast.As[map[string]int]([]any{"c", "3"}) if m2["c"] != 3 { - t.Errorf("AsMap failed: %v", m2) + t.Errorf("As[map] failed: %v", m2) } // ToSlice @@ -65,9 +83,9 @@ func TestMapSliceAPIs(t *testing.T) { t.Errorf("ToSlice failed: %v, %v", s, err) } - // AsSlice - s2 := cast.AsSlice[string]([]int{1, 2}) + // As[[]...] + s2 := cast.As[[]string]([]int{1, 2}) if len(s2) != 2 || s2[0] != "1" || s2[1] != "2" { - t.Errorf("AsSlice failed: %v", s2) + t.Errorf("As[[]string] failed: %v", s2) } }