diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bae7a2..40c96a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,12 @@ ## [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**: 核心 API `To[T]`, `ToJSON`, `FromJSON`, `ToMap`, `ToSlice` 全部重构为**不返回错误**,遇错静默返回零值。 +- **As 包装器**: 原 `Quiet` 更名为 `As`,用于一键消除传统 `(value, error)` 函数的摩擦。 ### Removed -- **YAML 支持**: 移除 `gopkg.in/yaml.v3` 依赖及所有 YAML 相关 API。 -- **API 整合**: 移除专门的 `AsXxx` 和 `MustXxx` 系列函数(如 `AsMap`, `AsJSON`, `MustToJSON` 等),推荐使用 `As[T]` 或 `Quiet/Must` 包装。 +- **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 diff --git a/README.md b/README.md index aac8914..0cab886 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,11 @@ ## 🎯 设计哲学 -`@go/cast` 是一个为“敏捷开发”设计的 Go 基础工具库。其核心目标是**消除类型摩擦**,让开发者在处理数据时更关注“我想要什么”而不是“原本是什么”。 +`@go/cast` 是一个为“极致敏捷”设计的 Go 基础工具库。其核心目标是**彻底消除摩擦**: -* **语义化 API**:通过 `To[T]` (严格/含错) 配合 `Quiet` (静默) 或 `Must` (断言) 提供一致的调用体验。 -* **智能穿透**:自动识别 JSON 文本、结构体映射、KV 展开等复杂场景。 +* **零错误返回**:核心 API 不返回 `error`,在失败或非法转换时静默返回类型零值。 +* **语义化 As 包装**:提供 `As` 函数用于将传统“值+错误”双返回结果一键转化为单值。 +* **智能自动穿透**:万能 `To[T]` 自动识别 JSON 文本、复杂容器映射、指针穿透等场景。 * **极致性能**:内置 FastEncoder/FastDecoder,单路径处理,最小化内存分配。 ## 📦 安装 @@ -22,55 +23,51 @@ go get apigo.cc/go/cast ```go import "apigo.cc/go/cast" -// 1. 语义化转换 (To[T] 返回错误) -val, err := cast.To[int]("123") // 123, nil +// 1. 万能转换 (To[T] 永不报错,遇错返回零值) +age := cast.To[int]("18") // 18 +uid := cast.To[int]("abc") // 0 (静默失败) -// 2. 摩擦消除工具 (Quiet 返回零值, Must 失败 Panic) -age := cast.Quiet(cast.To[int]("abc")) // 0 -uid := cast.Must(cast.To[int]("100")) // 100 +// 2. 传统函数消除摩擦 (As 将双返回包装为单值) +b := cast.As(json.Marshal(data)) // 返回 []byte,忽略 error -// 3. 快捷方式 (cast.As[T] 等价于 cast.Quiet(cast.To[T])) -name := cast.As[string](123) // "123" - -// 4. 智能 JSON 自动转换 +// 3. 智能 JSON 自动转换 // 输入 map/struct -> 目标 string: 自动序列化 -js := cast.As[string](map[string]int{"age": 18}) // `{"age":18}` +js := cast.To[string](map[string]int{"age": 18}) // `{"age":18}` // 输入 string/[]byte -> 目标 struct/map: 自动反序列化 -user, _ := cast.To[User](`{"name":"Tom"}`) +user := cast.To[User](`{"name":"Tom"}`) -// 5. 复杂容器转换 (Map/Slice) -m, _ := cast.ToMap[string, int]([]any{"id", "1", "score", 100}) -list := cast.As[[]int]([]string{"1", "2", "3"}) +// 4. 复杂容器转换 (Map/Slice) +m := cast.ToMap[string, int]([]any{"id", "1", "score", 100}) +list := cast.To[[]int]([]string{"1", "2", "3"}) ``` ## 🛠 API 指南 -### 核心语义 API +### 核心 API 1. **通用转换** - * `To[T any](any) (T, error)` —— 万能转换入口。支持基础类型、Slice、Map 以及 JSON 的双向自动转换。 - * `As[T any](any) T` —— 零摩擦转换快捷方式。失败时返回类型零值。 + * `To[T any](any) T` —— 万能转换入口。支持基础类型、Slice、Map 以及 JSON 的双向自动转换。失败时返回类型零值。 + * `As[T any](v T, err error) T` —— 错误消除工具。将传统的 `(value, error)` 返回值包装为单值。 -2. **错误处理工具** - * `Quiet[T any](T, error) T` —— 忽略错误,返回转换后的值(若有错则返回零值)。 - * `Must[T any](T, error) T` —— 严谨模式。若有错误则直接 Panic,常用于全局初始化。 - -3. **容器转换** - * `ToMap[K, V](any) (map[K]V, error)` —— 构建新 Map。 - * `ToSlice[T](any) ([]T, error)` —— 构建新 Slice。 +2. **容器转换** + * `ToMap[K, V](any) map[K]V` —— 构建新 Map。 + * `ToSlice[T](any) []T` —— 构建新 Slice。 * `FillMap(target, source)` | `FillSlice(target, source)` —— 填充现有容器。 -4. **JSON 序列化与构建** - * **编码**: `ToJSON(any) (string, error)` | `ToJSONBytes(any) ([]byte, error)` - * **解码**: `UnmarshalJSON(data, target) error` | `FromJSON[T](any) (T, error)` - * **进阶**: `ToJSONDesensitize(any, []string) (string, error)` (支持字段脱敏) +3. **JSON 序列化与构建** + * **编码**: `ToJSON(any) string` | `ToJSONBytes(any) []byte` + * **解码**: `UnmarshalJSON(data, target) error` (底层 API,保留 error 用于调试) | `FromJSON[T](any) T` + * **进阶**: `ToJSONDesensitizeBytes(any, []string) []byte` (支持字段脱敏) | `PrettyToJSON(any) string` -5. **泛型工具** +4. **泛型工具** * `If[T any](bool, T, T) T` —— 三元逻辑。 * `In[T comparable]([]T, T) bool` —— 包含判断。 * `Ptr[T any](T) *T` —— 快速取指针。 * `ArrayToBoolMap[T comparable]([]T) map[T]bool` —— 快速构建索引 Map。 +5. **基础转换 (直接调用,极致性能)** + * `Int`, `Int64`, `Uint`, `Uint64`, `Float`, `Float64`, `String`, `Bool`, `Duration` + ## 🧪 验证状态 测试全部通过,性能达标。详见:[TEST.md](./TEST.md) diff --git a/bench_test.go b/bench_test.go index 5e4c41d..c14884c 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.As[string](u) + _ = cast.To[string](u) } } @@ -147,7 +147,7 @@ func BenchmarkToJSON_SimpleStruct(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { // 这里包含了你特有的 makeJSONType 清洗逻辑和 FixUpperCase 逻辑 - _ = cast.As[string](u) + _ = cast.To[string](u) } } @@ -160,6 +160,6 @@ func BenchmarkToJSON_DirtyMap(b *testing.B) { } b.ResetTimer() for i := 0; i < b.N; i++ { - _ = cast.As[string](val) + _ = cast.To[string](val) } } diff --git a/cast.go b/cast.go index c6f4150..75e9ebd 100644 --- a/cast.go +++ b/cast.go @@ -25,8 +25,8 @@ func In[T comparable](arr []T, val T) bool { return slices.Contains(arr, val) } -// Quiet 忽略错误,返回零值 -func Quiet[T any](v T, err error) T { +// As 忽略错误,返回零值 (消除摩擦) +func As[T any](v T, err error) T { if err != nil { var zero T return zero @@ -34,14 +34,6 @@ func Quiet[T any](v T, err error) T { 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() { @@ -53,8 +45,8 @@ func RealValue(v reflect.Value) reflect.Value { } // --- Core Cast Logic --- -// To 泛型转换 (支持基础类型、Slice、Map 及 JSON 自动转换) -func To[T any](v any) (T, error) { +// To 泛型转换 (支持基础类型、Slice、Map 及 JSON 自动转换,零摩擦模式) +func To[T any](v any) T { var zero T targetType := reflect.TypeOf((*T)(nil)).Elem() @@ -65,11 +57,8 @@ func To[T any](v any) (T, error) { // 2. 处理 JSON 自动转换 (Input: struct/map/slice, Target: string/[]byte) if isComplexValue(v) && (targetType.Kind() == reflect.String || (targetType.Kind() == reflect.Slice && targetType.Elem().Kind() == reflect.Uint8)) { - s, err := ToJSON(v) - if err != nil { - return zero, err - } - return any(reflectCast(s, targetType).Interface()).(T), nil + s := ToJSON(v) + return any(reflectCast(s, targetType).Interface()).(T) } // 3. 处理 Slice/Map @@ -78,20 +67,20 @@ func To[T any](v any) (T, error) { ptr := reflect.New(targetType) ptr.Elem().Set(sv) fillToSlice(ptr.Elem(), v) - return ptr.Elem().Interface().(T), nil + return ptr.Elem().Interface().(T) } if targetType.Kind() == reflect.Map { mv := reflect.MakeMap(targetType) fillToMap(mv, v) - return mv.Interface().(T), nil + return mv.Interface().(T) } // 4. 处理基础类型 - res, err := reflectCastE(v, targetType) - if err != nil { - return zero, err + res := reflectCast(v, targetType) + if !res.IsValid() { + return zero } - return res.Interface().(T), nil + return res.Interface().(T) } func reflectCastE(value any, t reflect.Type) (reflect.Value, error) { @@ -190,11 +179,6 @@ func ToFloat64E(v any) (float64, error) { return f, nil } -// As 泛型转换 (失败返回零值) -func As[T any](v any) T { - return Quiet(To[T](v)) -} - func isJSONText(v any) bool { switch val := v.(type) { case string: @@ -448,41 +432,27 @@ func Duration(value any) time.Duration { return 0 } -func ToJSONBytes(value any) ([]byte, error) { - return fastToJSONBytes(value) +func ToJSONBytes(value any) []byte { + return As(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 { + return As(fastToJSONBytes(value, keys...)) } -func ToJSONDesensitizeBytes(value any, keys []string) ([]byte, error) { - return fastToJSONBytes(value, keys...) +func ToJSON(value any) string { + return string(ToJSONBytes(value)) } -func ToJSON(value any) (string, error) { - j, err := ToJSONBytes(value) - if err != nil { - return "", err - } - return string(j), nil -} - -func PrettyToJSONBytes(value any) []byte { - j := Quiet(ToJSONBytes(value)) +func PrettyToJSON(value any) string { + j := ToJSONBytes(value) r := &bytes.Buffer{} if err := json.Indent(r, j, "", " "); err == nil { - return r.Bytes() + return r.String() } - return j + return string(j) } -func PrettyToJSON(value any) string { return string(PrettyToJSONBytes(value)) } - func toBytes(data any) []byte { if data == nil { return nil @@ -504,10 +474,10 @@ func UnmarshalJSON(data any, value any) error { return fastUnmarshalJSONBytes(b, value) } -func FromJSON[T any](data any) (T, error) { +func FromJSON[T any](data any) T { var v T - err := UnmarshalJSON(data, &v) - return v, err + _ = UnmarshalJSON(data, &v) + return v } // --- Others (Keep logic but clean style) --- @@ -608,17 +578,17 @@ func ArrayToBoolMap[T comparable](arr []T) map[T]bool { } // ToMap 泛型构建新 Map -func ToMap[K comparable, V any](source any) (map[K]V, error) { +func ToMap[K comparable, V any](source any) map[K]V { m := make(map[K]V) fillToMap(reflect.ValueOf(m), source) - return m, nil + return m } // ToSlice 泛型构建新 Slice -func ToSlice[T any](source any) ([]T, error) { +func ToSlice[T any](source any) []T { var s []T fillToSlice(reflect.ValueOf(&s).Elem(), source) - return s, nil + return s } // FillMap 将 source 填充到目标 map 中 (兼容旧 API 逻辑) @@ -727,7 +697,7 @@ func fillToSlice(sliceRv reflect.Value, source any) { } func reflectCast(value any, t reflect.Type) reflect.Value { - return Quiet(reflectCastE(value, t)) + return As(reflectCastE(value, t)) } // 补充缺失的 Key 转换工具 diff --git a/cast_test.go b/cast_test.go index 2c9bbfd..4863a64 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.As[string](u) + js := cast.To[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.As[string](c) + js := cast.To[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.As[string](data) + js := cast.To[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.As[string](mockGojaArray) + js := cast.To[string](mockGojaArray) // 我们期望它被正确识别并转化为标准的 JSON 数组,且顺序不会乱 expected := `["apple","banana","cherry"]` @@ -194,7 +194,7 @@ func TestUnaddressableStruct(t *testing.T) { } }() - res := cast.As[string](d) + res := cast.To[string](d) if !strings.Contains(res, "\"name\"") { t.Errorf("Value struct ToJSON failed to lowercase key: %s", res) } @@ -203,24 +203,24 @@ func TestUnaddressableStruct(t *testing.T) { func TestToJSON_Nil(t *testing.T) { // Nil slice should be [] var s []int - if js := cast.As[string](s); js != "[]" { + if js := cast.To[string](s); js != "[]" { t.Errorf("Nil slice expected [], got %s", js) } // Nil map should be {} var m map[string]int - if js := cast.As[string](m); js != "{}" { + if js := cast.To[string](m); js != "{}" { t.Errorf("Nil map expected {}, got %s", js) } // Nil pointer should be null var p *int - if js := cast.As[string](p); js != "null" { + if js := cast.To[string](p); js != "null" { t.Errorf("Nil pointer expected null, got %s", js) } } -func TestToJSONDesensitize(t *testing.T) { +func TestToJSONDesensitizeBytes(t *testing.T) { type User struct { Name string Password string @@ -229,10 +229,8 @@ func TestToJSONDesensitize(t *testing.T) { u := User{Name: "Tom", Password: "secret123", Age: 18} // 测试脱敏功能 - js, err := cast.ToJSONDesensitize(u, []string{"password"}) - if err != nil { - t.Fatalf("ToJSONDesensitize failed: %v", err) - } + b := cast.ToJSONDesensitizeBytes(u, []string{"password"}) + js := string(b) if !strings.Contains(js, `"password":"***"`) { t.Errorf("Password should be desensitized, got: %s", js) @@ -247,7 +245,7 @@ func TestFastEncoder_MapAny(t *testing.T) { "userName": "admin", 123: "val", } - js := cast.As[string](data) + js := cast.To[string](data) if !strings.Contains(js, `"123":"val"`) || !strings.Contains(js, `"userName":"admin"`) { t.Errorf("MapAny encoding failed: %s", js) } @@ -258,7 +256,7 @@ func TestFastEncoder_GojaArray(t *testing.T) { 0: "a", 1: "b", } - js := cast.As[string](data) + js := cast.To[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 59da273..0242df2 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.As[map[int]string]([]any{"1", "a", 2, 200}) + m2 := cast.ToMap[int, string]([]any{"1", "a", 2, 200}) if m2[1] != "a" || m2[2] != "200" { t.Errorf("Slice-to-map failed: %v", m2) } @@ -58,27 +58,27 @@ func TestJSON(t *testing.T) { } // Test FromJSON - c2, err := cast.FromJSON[Config]([]byte(data)) - if err != nil || c2.Port != 8080 { - t.Errorf("FromJSON failed: %v, %v", err, c2) + c2 := cast.FromJSON[Config]([]byte(data)) + if c2.Port != 8080 { + t.Errorf("FromJSON failed: %v", c2) } - // Test As[T] from JSON - c3 := cast.As[Config](data) + // Test To[T] from JSON + c3 := cast.To[Config](data) if c3.Port != 8080 { - t.Errorf("As[Config] failed: %v", c3) + t.Errorf("To[Config] failed: %v", c3) } } -func TestGenericAsSlice(t *testing.T) { - // Test As with various inputs - s1 := cast.As[[]int]("123") +func TestGenericToSlice(t *testing.T) { + // Test To with various inputs + s1 := cast.To[[]int]("123") if len(s1) != 1 || s1[0] != 123 { - t.Errorf("As[[]int] primitive failed: %v", s1) + t.Errorf("To[[]int] primitive failed: %v", s1) } - s2 := cast.As[[]string]([]int{1, 2}) + s2 := cast.To[[]string]([]int{1, 2}) if len(s2) != 2 || s2[0] != "1" || s2[1] != "2" { - t.Errorf("As[[]string] slice failed: %v", s2) + t.Errorf("To[[]string] slice failed: %v", s2) } } diff --git a/semantic_test.go b/semantic_test.go index 6e0857f..cd488cb 100644 --- a/semantic_test.go +++ b/semantic_test.go @@ -5,28 +5,25 @@ import ( "apigo.cc/go/cast" ) -func TestToAs(t *testing.T) { +func TestTo(t *testing.T) { // Basic types - if v, err := cast.To[int]("123"); err != nil || v != 123 { - t.Errorf("To[int] failed: %v, %v", v, err) + if v := cast.To[int]("123"); v != 123 { + t.Errorf("To[int] failed: %v", v) } - if v := cast.As[int]("123"); v != 123 { - t.Errorf("As[int] failed: %v", v) - } - if v := cast.As[int]("abc"); v != 0 { - t.Errorf("As[int] for invalid input should be 0, got %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, err := cast.To[[]int]([]string{"1", "2", "3"}) - if err != nil || len(s) != 3 || s[0] != 1 || s[1] != 2 || s[2] != 3 { - t.Errorf("To[[]int] failed: %v, %v", s, err) + 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, err := cast.To[map[string]int](map[string]string{"a": "1", "b": "2"}) - if err != nil || len(m) != 2 || m["a"] != 1 || m["b"] != 2 { - t.Errorf("To[map[string]int] failed: %v, %v", m, err) + 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) @@ -34,58 +31,35 @@ func TestToAs(t *testing.T) { Name string `json:"name"` } u := User{Name: "Alice"} - js, err := cast.To[string](u) - if err != nil || js != `{"name":"Alice"}` { - t.Errorf("To[string] for struct failed: %v, %v", js, err) + 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, err := cast.To[User](`{"name":"Bob"}`) - if err != nil || u2.Name != "Bob" { - t.Errorf("To[User] from string failed: %v, %v", u2, err) + u2 := cast.To[User](`{"name":"Bob"}`) + if u2.Name != "Bob" { + t.Errorf("To[User] from string failed: %v", u2) } } -func TestQuietMust(t *testing.T) { - // Quiet - if v := cast.Quiet(cast.To[int]("abc")); v != 0 { - t.Errorf("Quiet failed: %v", v) +func TestAsWrapper(t *testing.T) { + // As + if v := cast.As(123, nil); v != 123 { + t.Errorf("As 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}) - if err != nil || m["a"] != 1 || m["b"] != 2 { - t.Errorf("ToMap failed: %v, %v", m, err) - } - - // As[map...] - m2 := cast.As[map[string]int]([]any{"c", "3"}) - if m2["c"] != 3 { - t.Errorf("As[map] failed: %v", m2) + m := cast.ToMap[string, int]([]any{"a", "1", "b", 2}) + if m["a"] != 1 || m["b"] != 2 { + t.Errorf("ToMap failed: %v", m) } // ToSlice - s, err := cast.ToSlice[int]([]string{"10", "20"}) - if err != nil || len(s) != 2 || s[0] != 10 || s[1] != 20 { - t.Errorf("ToSlice failed: %v, %v", s, err) - } - - // As[[]...] - s2 := cast.As[[]string]([]int{1, 2}) - if len(s2) != 2 || s2[0] != "1" || s2[1] != "2" { - t.Errorf("As[[]string] failed: %v", s2) + s := cast.ToSlice[int]([]string{"10", "20"}) + if len(s) != 2 || s[0] != 10 || s[1] != 20 { + t.Errorf("ToSlice failed: %v", s) } }