mirror of
https://gitee.com/johng/gf
synced 2026-06-06 16:21:40 +08:00
feat(util/gconv): Add OmitEmpty and OmitNil options to Scan function (#4584)
## 改进内容
- 扩展 `ScanOption`/`StructOption` 结构体,添加 `OmitEmpty bool` 字段:当设置为 true
时,跳过空值(如空字符串、零值等)的赋值;添加 `OmitNil bool` 字段:当设置为 true 时,跳过 nil 值的赋值;
- 添加 `ScanWithOptions` 函数,支持通过 `ScanOption` 参数使用新选项
- 原有的 `Scan` 函数行为完全不变
- 通过 `NewConverter` 创建的转换器也支持新功能
## 使用示例
### 基本用法
```go
type User struct {
Name *string
Age int
Email string
}
type Person struct {
Name string
Age int
Email string
}
user := User{Name: nil, Age: 25, Email: ""}
person := Person{Name: "zhangsan", Age: 0, Email: "old@example.com"}
err := gconv.ScanWithOptions(user, &person, gconv.ScanOption{
OmitEmpty: true,
OmitNil: true,
})
// 结果: person.Name 保持 "zhangsan",person.Age 变为 25,person.Email 保持 "old@example.com"
```
后续可以将`func Scan(srcValue any, dstPointer any, paramKeyToAttrMap
...map[string]string) (err error)`和`func ScanWithOptions(srcValue any,
dstPointer any, option ...ScanOption) (err error)`直接用`func Scan(srcValue
any, dstPointer any, option ...ScanOption) (err
error)`代替,`ScanOption`里已经包含了`paramKeyToAttrMap map[string]string`
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@ -25,3 +25,35 @@ func Scan(srcValue any, dstPointer any, paramKeyToAttrMap ...map[string]string)
|
||||
}
|
||||
return defaultConverter.Scan(srcValue, dstPointer, option)
|
||||
}
|
||||
|
||||
// ScanWithOptions automatically checks the type of `dstPointer` and converts `srcValue` to `dstPointer`.
|
||||
// It is the same as Scan function, but accepts one or more ScanOption values for additional conversion control.
|
||||
//
|
||||
// When using ScanWithOptions, the term "omit" means that the assignment from the source to the destination
|
||||
// is skipped, so the existing value in the destination field is preserved.
|
||||
//
|
||||
// - option.OmitEmpty, when set to true, skips assignment of empty source values (for example: empty strings,
|
||||
// zero numeric values, zero time values, empty slices or maps), preserving any existing non-empty values
|
||||
// in the destination.
|
||||
//
|
||||
// - option.OmitNil, when set to true, skips assignment of nil source values, preserving the existing values
|
||||
// in the destination when the source contains nil.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// dst := &User{Name: "Alice", Email: "alice@example.com"}
|
||||
// src := map[string]any{
|
||||
// "Name": "",
|
||||
// "Email": nil,
|
||||
// }
|
||||
//
|
||||
// // With OmitEmpty and OmitNil, empty and nil values in src will not overwrite dst.
|
||||
// err := ScanWithOptions(src, dst, ScanOption{OmitEmpty: true, OmitNil: true})
|
||||
func ScanWithOptions(srcValue any, dstPointer any, option ...ScanOption) (err error) {
|
||||
return defaultConverter.Scan(srcValue, dstPointer, option...)
|
||||
}
|
||||
|
||||
146
util/gconv/gconv_z_unit_scan_omit_test.go
Normal file
146
util/gconv/gconv_z_unit_scan_omit_test.go
Normal file
@ -0,0 +1,146 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gconv_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
type User2 struct {
|
||||
Name *string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
func TestScan_OmitEmpty(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := User{Name: "", Age: 20, Email: ""}
|
||||
person := Person{Name: "zhangsan", Age: 0, Email: "old@example.com"}
|
||||
|
||||
err := gconv.ScanWithOptions(user, &person, gconv.ScanOption{
|
||||
OmitEmpty: true,
|
||||
})
|
||||
t.AssertNil(err)
|
||||
t.Assert(person.Name, "zhangsan")
|
||||
t.Assert(person.Age, 20)
|
||||
t.Assert(person.Email, "old@example.com")
|
||||
})
|
||||
}
|
||||
|
||||
func TestScan_AllOmitEmpty(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := User{Name: "", Age: 0, Email: ""}
|
||||
person := Person{Name: "zhangsan", Age: 100, Email: "old@example.com"}
|
||||
|
||||
err := gconv.ScanWithOptions(user, &person, gconv.ScanOption{
|
||||
OmitEmpty: true,
|
||||
})
|
||||
t.AssertNil(err)
|
||||
t.Assert(person.Name, "zhangsan")
|
||||
t.Assert(person.Age, 100)
|
||||
t.Assert(person.Email, "old@example.com")
|
||||
})
|
||||
}
|
||||
|
||||
func TestScan_OmitNil(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := map[string]any{
|
||||
"Name": nil,
|
||||
"Age": 30,
|
||||
"Email": nil,
|
||||
}
|
||||
person := Person{Name: "lisi", Age: 0, Email: "old@example.com"}
|
||||
|
||||
err := gconv.ScanWithOptions(data, &person, gconv.ScanOption{
|
||||
OmitNil: true,
|
||||
})
|
||||
t.AssertNil(err)
|
||||
t.Assert(person.Name, "lisi")
|
||||
t.Assert(person.Age, 30)
|
||||
t.Assert(person.Email, "old@example.com")
|
||||
})
|
||||
}
|
||||
|
||||
func TestScan_OmitEmptyAndOmitNil(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := map[string]any{
|
||||
"Name": "",
|
||||
"Age": 25,
|
||||
"Email": nil,
|
||||
}
|
||||
person := Person{Name: "wangwu", Age: 0, Email: "old2@example.com"}
|
||||
|
||||
err := gconv.ScanWithOptions(data, &person, gconv.ScanOption{
|
||||
OmitEmpty: true,
|
||||
OmitNil: true,
|
||||
})
|
||||
t.AssertNil(err)
|
||||
t.Assert(person.Name, "wangwu")
|
||||
t.Assert(person.Age, 25)
|
||||
t.Assert(person.Email, "old2@example.com")
|
||||
})
|
||||
}
|
||||
|
||||
func TestScan_NoOmitOptions(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := User{Name: "", Age: 20, Email: ""}
|
||||
person := Person{Name: "zhangsan", Age: 30, Email: "old@example.com"}
|
||||
|
||||
err := gconv.ScanWithOptions(user, &person, gconv.ScanOption{
|
||||
OmitEmpty: false,
|
||||
OmitNil: false,
|
||||
})
|
||||
t.AssertNil(err)
|
||||
t.Assert(person.Name, "")
|
||||
t.Assert(person.Age, 20)
|
||||
t.Assert(person.Email, "")
|
||||
})
|
||||
}
|
||||
|
||||
func TestScan_OriginalBehavior(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := User{Name: "newname", Age: 25, Email: "new@example.com"}
|
||||
person := Person{Name: "", Age: 0, Email: ""}
|
||||
|
||||
err := gconv.Scan(user, &person)
|
||||
t.AssertNil(err)
|
||||
t.Assert(person.Name, "newname")
|
||||
t.Assert(person.Age, 25)
|
||||
t.Assert(person.Email, "new@example.com")
|
||||
})
|
||||
}
|
||||
|
||||
func TestScan_StructOmitEmptyAndOmitNilOptions(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user2 := User2{Name: nil, Age: 25, Email: ""}
|
||||
person := Person{Name: "wangwu", Age: 0, Email: "old2@example.com"}
|
||||
|
||||
err := gconv.ScanWithOptions(user2, &person, gconv.ScanOption{
|
||||
OmitEmpty: true,
|
||||
OmitNil: true,
|
||||
})
|
||||
t.AssertNil(err)
|
||||
t.Assert(person.Name, "wangwu")
|
||||
t.Assert(person.Age, 25)
|
||||
t.Assert(person.Email, "old2@example.com")
|
||||
})
|
||||
}
|
||||
@ -23,6 +23,15 @@ type ScanOption struct {
|
||||
// ContinueOnError specifies whether to continue converting the next element
|
||||
// if one element converting fails.
|
||||
ContinueOnError bool
|
||||
|
||||
// OmitEmpty specifies whether to skip assignment when the source value is empty
|
||||
// (empty string, zero value, etc.), preserving the existing value in the
|
||||
// destination field.
|
||||
OmitEmpty bool
|
||||
|
||||
// OmitNil specifies whether to skip assignment when the source value is nil,
|
||||
// preserving the existing value in the destination field.
|
||||
OmitNil bool
|
||||
}
|
||||
|
||||
func (c *Converter) getScanOption(option ...ScanOption) ScanOption {
|
||||
@ -292,6 +301,8 @@ func (c *Converter) doScanForComplicatedTypes(
|
||||
mapOption = StructOption{
|
||||
ParamKeyToAttrMap: keyToAttributeNameMapping,
|
||||
ContinueOnError: option.ContinueOnError,
|
||||
OmitEmpty: option.OmitEmpty,
|
||||
OmitNil: option.OmitNil,
|
||||
}
|
||||
)
|
||||
return c.Structs(srcValue, dstPointer, StructsOption{
|
||||
@ -304,6 +315,8 @@ func (c *Converter) doScanForComplicatedTypes(
|
||||
ParamKeyToAttrMap: keyToAttributeNameMapping,
|
||||
PriorityTag: "",
|
||||
ContinueOnError: option.ContinueOnError,
|
||||
OmitEmpty: option.OmitEmpty,
|
||||
OmitNil: option.OmitNil,
|
||||
}
|
||||
return c.Struct(srcValue, dstPointer, structOption)
|
||||
}
|
||||
|
||||
@ -30,6 +30,15 @@ type StructOption struct {
|
||||
// ContinueOnError specifies whether to continue converting the next element
|
||||
// if one element converting fails.
|
||||
ContinueOnError bool
|
||||
|
||||
// OmitEmpty specifies whether to skip assignment when the source value is empty
|
||||
// (empty string, zero value, etc.), preserving the existing value in the
|
||||
// destination field.
|
||||
OmitEmpty bool
|
||||
|
||||
// OmitNil specifies whether to skip assignment when the source value is nil,
|
||||
// preserving the existing value in the destination field.
|
||||
OmitNil bool
|
||||
}
|
||||
|
||||
func (c *Converter) getStructOption(option ...StructOption) StructOption {
|
||||
@ -363,6 +372,13 @@ func (c *Converter) bindVarToStructField(
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Check if the value should be omitted based on OmitEmpty or OmitNil options
|
||||
if option.OmitNil && empty.IsNil(srcValue) {
|
||||
return nil
|
||||
}
|
||||
if option.OmitEmpty && empty.IsEmpty(srcValue) {
|
||||
return nil
|
||||
}
|
||||
// Directly converting.
|
||||
if empty.IsNil(srcValue) {
|
||||
fieldValue.Set(reflect.Zero(fieldValue.Type()))
|
||||
|
||||
Reference in New Issue
Block a user