feat: 增强 nil 值处理能力
主要改进: - 简化 setValue 方法中的 nil 值处理逻辑,统一设置为目标类型的零值 - 修复 setFieldValue 方法中 map 类型设置 nil 值时的问题 - 新增完整的 nil 值设置测试,覆盖所有 Go 基础类型和复合类型 测试覆盖: - 指针、接口、切片、map、channel、func 等可空类型设置为 nil - int/uint 系列、float 系列、bool、string 等基础类型设置为零值 - 嵌套结构体和 map 中的 nil 值设置
This commit is contained in:
parent
6e3593f188
commit
fbba6f9a30
178
rfx.go
178
rfx.go
@ -209,6 +209,13 @@ func (r *rfx) setFieldValue(target reflect.Value, key string, v any) bool {
|
||||
if target.IsNil() {
|
||||
target.Set(reflect.MakeMap(target.Type()))
|
||||
}
|
||||
|
||||
// 处理 nil 值的情况
|
||||
if v == nil {
|
||||
target.SetMapIndex(reflect.ValueOf(key), reflect.Zero(target.Type().Elem()))
|
||||
return true
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(v)
|
||||
if !val.Type().AssignableTo(target.Type().Elem()) {
|
||||
// 尝试转换
|
||||
@ -275,106 +282,97 @@ func (r *rfx) setValue(field reflect.Value, v any) bool {
|
||||
targetType := field.Type()
|
||||
|
||||
if !val.IsValid() {
|
||||
// 如果值无效(通常是 nil 的情况)
|
||||
// 对于可以为 nil 的类型,直接设置零值
|
||||
switch targetType.Kind() {
|
||||
case reflect.Ptr, reflect.Interface, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func:
|
||||
field.Set(reflect.Zero(targetType))
|
||||
return true
|
||||
default:
|
||||
// 对于基础类型,继续使用后面的 cast 转换逻辑
|
||||
// 不提前返回 false
|
||||
}
|
||||
} else {
|
||||
// 如果 val 有效,进行正常的类型处理
|
||||
// 统一解开最外层的 interface 包装,便于后续根据底层实际类型做处理
|
||||
for val.Kind() == reflect.Interface && !val.IsNil() {
|
||||
val = val.Elem()
|
||||
}
|
||||
val = reflect.Zero(targetType)
|
||||
}
|
||||
|
||||
// 尝试直接赋值(类型完全匹配)
|
||||
// 如果 val 有效,进行正常的类型处理
|
||||
// 统一解开最外层的 interface 包装,便于后续根据底层实际类型做处理
|
||||
for val.Kind() == reflect.Interface && !val.IsNil() {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
// 尝试直接赋值(类型完全匹配)
|
||||
if val.Type().AssignableTo(targetType) {
|
||||
field.Set(val)
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果源值是指针但目标不是指针,尝试解引用后再赋值
|
||||
if val.Kind() == reflect.Ptr && !val.IsNil() && targetType.Kind() != reflect.Ptr {
|
||||
derefVal := val.Elem()
|
||||
if derefVal.Type().AssignableTo(targetType) {
|
||||
field.Set(derefVal)
|
||||
return true
|
||||
}
|
||||
// 解引用后继续使用下面的逻辑处理
|
||||
val = derefVal
|
||||
}
|
||||
|
||||
switch targetType.Kind() {
|
||||
case reflect.Ptr:
|
||||
// 处理指针类型
|
||||
// 如果传入的值已经是指针类型,尝试直接赋值
|
||||
if val.Type().AssignableTo(targetType) {
|
||||
field.Set(val)
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果源值是指针但目标不是指针,尝试解引用后再赋值
|
||||
if val.Kind() == reflect.Ptr && !val.IsNil() && targetType.Kind() != reflect.Ptr {
|
||||
derefVal := val.Elem()
|
||||
if derefVal.Type().AssignableTo(targetType) {
|
||||
field.Set(derefVal)
|
||||
return true
|
||||
}
|
||||
// 解引用后继续使用下面的逻辑处理
|
||||
val = derefVal
|
||||
// 如果传入的值不是指针,创建新指针并设置值
|
||||
elemType := targetType.Elem()
|
||||
newPtr := reflect.New(elemType)
|
||||
|
||||
// 递归设置指针指向的值
|
||||
if !r.setValue(newPtr.Elem(), v) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch targetType.Kind() {
|
||||
case reflect.Ptr:
|
||||
// 处理指针类型
|
||||
// 如果传入的值已经是指针类型,尝试直接赋值
|
||||
if val.Type().AssignableTo(targetType) {
|
||||
field.Set(val)
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果传入的值不是指针,创建新指针并设置值
|
||||
elemType := targetType.Elem()
|
||||
newPtr := reflect.New(elemType)
|
||||
|
||||
// 递归设置指针指向的值
|
||||
if !r.setValue(newPtr.Elem(), v) {
|
||||
return false
|
||||
}
|
||||
|
||||
field.Set(newPtr)
|
||||
return true
|
||||
|
||||
case reflect.Slice: // 处理切片类型,支持从通用切片(如 []any)转换
|
||||
if val.Kind() != reflect.Slice && val.Kind() != reflect.Array {
|
||||
return false
|
||||
}
|
||||
|
||||
newSlice := reflect.MakeSlice(targetType, val.Len(), val.Len())
|
||||
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
if !r.setValue(newSlice.Index(i), val.Index(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
field.Set(newSlice)
|
||||
|
||||
return true
|
||||
|
||||
case reflect.Struct:
|
||||
// 遍历目标结构体的字段,从源值(结构体或 map)中按字段名取值并设置
|
||||
// 仅支持从 struct 或 map 填充
|
||||
if val.Kind() != reflect.Struct && val.Kind() != reflect.Map {
|
||||
return false
|
||||
}
|
||||
|
||||
fieldType := field.Type()
|
||||
for i := 0; i < fieldType.NumField(); i++ {
|
||||
dstField := field.Field(i)
|
||||
if !dstField.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
valField := getValueByPath(val, fieldType.Field(i).Name)
|
||||
|
||||
if !valField.IsValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 使用 setValue 复用现有的类型转换逻辑,忽略单个字段失败
|
||||
if !r.setValue(dstField, valField) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
field.Set(newPtr)
|
||||
return true
|
||||
|
||||
case reflect.Slice: // 处理切片类型,支持从通用切片(如 []any)转换
|
||||
if val.Kind() != reflect.Slice && val.Kind() != reflect.Array {
|
||||
return false
|
||||
}
|
||||
|
||||
newSlice := reflect.MakeSlice(targetType, val.Len(), val.Len())
|
||||
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
if !r.setValue(newSlice.Index(i), val.Index(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
field.Set(newSlice)
|
||||
|
||||
return true
|
||||
|
||||
case reflect.Struct:
|
||||
// 遍历目标结构体的字段,从源值(结构体或 map)中按字段名取值并设置
|
||||
// 仅支持从 struct 或 map 填充
|
||||
if val.Kind() != reflect.Struct && val.Kind() != reflect.Map {
|
||||
return false
|
||||
}
|
||||
|
||||
fieldType := field.Type()
|
||||
for i := 0; i < fieldType.NumField(); i++ {
|
||||
dstField := field.Field(i)
|
||||
if !dstField.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
valField := getValueByPath(val, fieldType.Field(i).Name)
|
||||
|
||||
if !valField.IsValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 使用 setValue 复用现有的类型转换逻辑,忽略单个字段失败
|
||||
if !r.setValue(dstField, valField) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// 优先使用 cast 进行智能类型转换
|
||||
|
||||
327
rfx_nil_test.go
Normal file
327
rfx_nil_test.go
Normal file
@ -0,0 +1,327 @@
|
||||
package reflux
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSetValueWithNil 测试 setValue 方法处理 nil 值的各种情况
|
||||
func TestSetValueWithNil(t *testing.T) {
|
||||
t.Run("Set nil to pointer field", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Ptr *string
|
||||
}
|
||||
data := Data{Ptr: stringPtr("initial")}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Ptr", nil)
|
||||
if data.Ptr != nil {
|
||||
t.Errorf("Expected Ptr to be nil, got %v", data.Ptr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to interface field", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Iface interface{}
|
||||
}
|
||||
data := Data{Iface: "initial"}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Iface", nil)
|
||||
if data.Iface != nil {
|
||||
t.Errorf("Expected Iface to be nil, got %v", data.Iface)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to slice field", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Slice []string
|
||||
}
|
||||
data := Data{Slice: []string{"a", "b"}}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Slice", nil)
|
||||
if data.Slice != nil {
|
||||
t.Errorf("Expected Slice to be nil, got %v", data.Slice)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to map field", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Map map[string]string
|
||||
}
|
||||
data := Data{Map: map[string]string{"key": "value"}}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Map", nil)
|
||||
if data.Map != nil {
|
||||
t.Errorf("Expected Map to be nil, got %v", data.Map)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to int field - should set to zero value", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value int
|
||||
}
|
||||
data := Data{Value: 42}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Value", nil)
|
||||
if data.Value != 0 {
|
||||
t.Errorf("Expected Value to be 0, got %d", data.Value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to string field - should set to zero value", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value string
|
||||
}
|
||||
data := Data{Value: "hello"}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Value", nil)
|
||||
if data.Value != "" {
|
||||
t.Errorf("Expected Value to be empty string, got %s", data.Value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to bool field - should set to zero value", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value bool
|
||||
}
|
||||
data := Data{Value: true}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Value", nil)
|
||||
if data.Value != false {
|
||||
t.Errorf("Expected Value to be false, got %v", data.Value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to float64 field - should set to zero value", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value float64
|
||||
}
|
||||
data := Data{Value: 3.14}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Value", nil)
|
||||
if data.Value != 0.0 {
|
||||
t.Errorf("Expected Value to be 0.0, got %f", data.Value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to uint field - should set to zero value", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value uint
|
||||
}
|
||||
data := Data{Value: 100}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Value", nil)
|
||||
if data.Value != 0 {
|
||||
t.Errorf("Expected Value to be 0, got %d", data.Value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to nested pointer field", func(t *testing.T) {
|
||||
type Inner struct {
|
||||
Value string
|
||||
}
|
||||
type Outer struct {
|
||||
Inner *Inner
|
||||
}
|
||||
data := Outer{Inner: &Inner{Value: "test"}}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Inner", nil)
|
||||
if data.Inner != nil {
|
||||
t.Errorf("Expected Inner to be nil, got %v", data.Inner)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil in map", func(t *testing.T) {
|
||||
m := map[string]interface{}{
|
||||
"ptr": stringPtr("test"),
|
||||
"slice": []int{1, 2, 3},
|
||||
"map": map[string]string{"key": "value"},
|
||||
}
|
||||
rfx := New(&m)
|
||||
|
||||
rfx.Set("ptr", nil)
|
||||
rfx.Set("slice", nil)
|
||||
rfx.Set("map", nil)
|
||||
|
||||
if m["ptr"] != nil {
|
||||
t.Errorf("Expected ptr to be nil, got %v", m["ptr"])
|
||||
}
|
||||
if m["slice"] != nil {
|
||||
t.Errorf("Expected slice to be nil, got %v", m["slice"])
|
||||
}
|
||||
if m["map"] != nil {
|
||||
t.Errorf("Expected map to be nil, got %v", m["map"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to various int types", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Int8 int8
|
||||
Int16 int16
|
||||
Int32 int32
|
||||
Int64 int64
|
||||
Uint8 uint8
|
||||
Uint16 uint16
|
||||
Uint32 uint32
|
||||
Uint64 uint64
|
||||
}
|
||||
data := Data{
|
||||
Int8: 127,
|
||||
Int16: 32767,
|
||||
Int32: 2147483647,
|
||||
Int64: 9223372036854775807,
|
||||
Uint8: 255,
|
||||
Uint16: 65535,
|
||||
Uint32: 4294967295,
|
||||
Uint64: 18446744073709551615,
|
||||
}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Int8", nil)
|
||||
rfx.Set("Int16", nil)
|
||||
rfx.Set("Int32", nil)
|
||||
rfx.Set("Int64", nil)
|
||||
rfx.Set("Uint8", nil)
|
||||
rfx.Set("Uint16", nil)
|
||||
rfx.Set("Uint32", nil)
|
||||
rfx.Set("Uint64", nil)
|
||||
|
||||
if data.Int8 != 0 {
|
||||
t.Errorf("Expected Int8 to be 0, got %d", data.Int8)
|
||||
}
|
||||
if data.Int16 != 0 {
|
||||
t.Errorf("Expected Int16 to be 0, got %d", data.Int16)
|
||||
}
|
||||
if data.Int32 != 0 {
|
||||
t.Errorf("Expected Int32 to be 0, got %d", data.Int32)
|
||||
}
|
||||
if data.Int64 != 0 {
|
||||
t.Errorf("Expected Int64 to be 0, got %d", data.Int64)
|
||||
}
|
||||
if data.Uint8 != 0 {
|
||||
t.Errorf("Expected Uint8 to be 0, got %d", data.Uint8)
|
||||
}
|
||||
if data.Uint16 != 0 {
|
||||
t.Errorf("Expected Uint16 to be 0, got %d", data.Uint16)
|
||||
}
|
||||
if data.Uint32 != 0 {
|
||||
t.Errorf("Expected Uint32 to be 0, got %d", data.Uint32)
|
||||
}
|
||||
if data.Uint64 != 0 {
|
||||
t.Errorf("Expected Uint64 to be 0, got %d", data.Uint64)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to float types", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Float32 float32
|
||||
Float64 float64
|
||||
}
|
||||
data := Data{
|
||||
Float32: 3.14,
|
||||
Float64: 2.718,
|
||||
}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Float32", nil)
|
||||
rfx.Set("Float64", nil)
|
||||
|
||||
if data.Float32 != 0.0 {
|
||||
t.Errorf("Expected Float32 to be 0.0, got %f", data.Float32)
|
||||
}
|
||||
if data.Float64 != 0.0 {
|
||||
t.Errorf("Expected Float64 to be 0.0, got %f", data.Float64)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to channel field", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Chan chan int
|
||||
}
|
||||
ch := make(chan int)
|
||||
data := Data{Chan: ch}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Chan", nil)
|
||||
if data.Chan != nil {
|
||||
t.Errorf("Expected Chan to be nil, got %v", data.Chan)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to func field", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Func func()
|
||||
}
|
||||
data := Data{Func: func() {}}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Func", nil)
|
||||
if data.Func != nil {
|
||||
t.Errorf("Expected Func to be nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set nil to nested struct with various types", func(t *testing.T) {
|
||||
type Inner struct {
|
||||
Ptr *string
|
||||
Slice []int
|
||||
Map map[string]string
|
||||
Int int
|
||||
String string
|
||||
Bool bool
|
||||
}
|
||||
type Outer struct {
|
||||
Inner Inner
|
||||
}
|
||||
data := Outer{
|
||||
Inner: Inner{
|
||||
Ptr: stringPtr("test"),
|
||||
Slice: []int{1, 2, 3},
|
||||
Map: map[string]string{"key": "value"},
|
||||
Int: 42,
|
||||
String: "hello",
|
||||
Bool: true,
|
||||
},
|
||||
}
|
||||
rfx := New(&data)
|
||||
|
||||
rfx.Set("Inner.Ptr", nil)
|
||||
rfx.Set("Inner.Slice", nil)
|
||||
rfx.Set("Inner.Map", nil)
|
||||
rfx.Set("Inner.Int", nil)
|
||||
rfx.Set("Inner.String", nil)
|
||||
rfx.Set("Inner.Bool", nil)
|
||||
|
||||
if data.Inner.Ptr != nil {
|
||||
t.Errorf("Expected Inner.Ptr to be nil, got %v", data.Inner.Ptr)
|
||||
}
|
||||
if data.Inner.Slice != nil {
|
||||
t.Errorf("Expected Inner.Slice to be nil, got %v", data.Inner.Slice)
|
||||
}
|
||||
if data.Inner.Map != nil {
|
||||
t.Errorf("Expected Inner.Map to be nil, got %v", data.Inner.Map)
|
||||
}
|
||||
if data.Inner.Int != 0 {
|
||||
t.Errorf("Expected Inner.Int to be 0, got %d", data.Inner.Int)
|
||||
}
|
||||
if data.Inner.String != "" {
|
||||
t.Errorf("Expected Inner.String to be empty, got %s", data.Inner.String)
|
||||
}
|
||||
if data.Inner.Bool != false {
|
||||
t.Errorf("Expected Inner.Bool to be false, got %v", data.Inner.Bool)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to create string pointer
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user