跳到主要内容

Mapx

mapx 包提供 Go 结构体与 map[string]any 之间的双向转换,底层基于 github.com/go-viper/mapstructure/v2。VEF 覆盖了上游的默认 tag —— 框架默认使用 json tag,并启用 mapstructure 的 squashing,使用 inline tag option。

API 参考

APIContract
mapx.DecoderHook默认 decoder 使用的公开 composed mapstructure.DecodeHookFunc
mapx.DecoderOption修改 mapstructure.DecoderConfig 的 function option 类型
mapx.Metadatamapstructure.Metadata 的 alias
mapx.NewDecoder(result, options...)使用 VEF 默认值创建 mapstructure.Decoder,然后按顺序应用 options
mapx.ToMap(value, options...)将 struct 或 pointer-to-struct 转成 map[string]any;非 struct 输入返回 ErrInvalidToMapValue
mapx.FromMap[T](value, options...)map[string]any 转成 *T;非 struct T 返回 ErrInvalidFromMapType
mapx.WithTagName(tagName)设置 DecoderConfig.TagName;默认是 json
mapx.WithIgnoreUntaggedFields(ignore)DecoderConfig.IgnoreUntaggedFields 设为传入的 boolean
mapx.WithDecodeHook(decodeHook)替换 DecoderConfig.DecodeHook;如需保留默认 hook,需要自行和 mapx.DecoderHook compose
mapx.WithMatchName(matchName)替换 key/field matcher;默认是 mapKey == lo.CamelCase(fieldName)
mapx.WithErrorUnused()设置 ErrorUnused = true
mapx.WithErrorUnset()设置 ErrorUnset = true
mapx.WithZeroFields()设置 ZeroFields = true
mapx.WithAllowUnsetPointer()设置 AllowUnsetPointer = true
mapx.WithMetadata(metadata)将 decode metadata 写入传入的 *mapx.Metadata
mapx.WithWeaklyTypedInput()设置 WeaklyTypedInput = true
mapx.WithDecodeNil()设置 DecodeNil = true
mapx.ErrInvalidToMapValueToMap 输入不是 struct 或 pointer-to-struct 时的 sentinel
mapx.ErrInvalidFromMapTypeFromMap[T]T 不是 struct 时的 sentinel
mapx.ErrCollectionSetNilElement解码到 collection set 时遇到 nil element 的 sentinel
mapx.ErrCollectionSetIncompatibleKindcollection set element 发生 string/numeric family mismatch 的 sentinel
mapx.ErrCollectionSetOverflowcollection set element 数值转换溢出的 sentinel
mapx.ErrCollectionSetNonIntegerfractional float 目标是 integer set element 时的 sentinel
mapx.ErrCollectionSetNotFiniteNaN 或 infinity 目标是 integer set element 时的 sentinel
mapx.ErrCollectionSetNegative负数目标是 unsigned set element 时的 sentinel
mapx.ErrCollectionSetUnsupportedTargetcollection set element kind 没有转换策略时的 sentinel
mapx.ErrJSONNumberNotInteger小数或指数形式的 json.Number 落到整数字段时的 sentinel
mapx.ErrJSONNumberOverflowjson.Number 超出数值目标类型范围时的 sentinel

结构体转 Map

import "github.com/coldsmirk/vef-framework-go/mapx"

type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}

user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
m, err := mapx.ToMap(user)
// m = map[string]any{"name": "Alice", "email": "alice@example.com", "age": 30}

Map 转结构体

data := map[string]any{
"name": "Bob",
"email": "bob@example.com",
"age": 25,
}

user, err := mapx.FromMap[User](data)
// user.Name = "Bob", user.Email = "bob@example.com", user.Age = 25

解码器选项

ToMapFromMap 都接受可选的 DecoderOption

// 切到其他 tag,例如 yaml
m, err := mapx.ToMap(user, mapx.WithTagName("yaml"))

// 弱类型转换(字符串 "123" → int 123)
user, err := mapx.FromMap[User](data, mapx.WithWeaklyTypedInput())

// 把源 map 里有但结构体里没有的字段当错误抛出
user, err := mapx.FromMap[User](data, mapx.WithErrorUnused())

可用选项

选项效果
WithTagName(tag)覆盖 mapx 读取的结构体 tag(默认 json)。
WithIgnoreUntaggedFields(ignore)设置是否跳过没有当前 tag 的字段。
WithDecodeHook(hook)替换默认 decode hook。
WithMatchName(fn)自定义字段名匹配函数(默认与 lo.CamelCase(fieldName) 精确比较)。
WithErrorUnused()源 map 里有结构体没有的字段时报错。
WithErrorUnset()结构体里有源 map 没有的字段时报错。
WithZeroFields()解码前清零目标结构体字段。
WithAllowUnsetPointer()允许指针字段保持 nil 而不被初始化。
WithMetadata(m)把 "unused" / "unset" 键收集到 mapstructure.Metadata
WithWeaklyTypedInput()常见类型间互转(string ↔ number ↔ bool …)。
WithDecodeNil()nil 源值送入解码管线(默认会被跳过)。

自定义解码器

高级场景可创建可复用的解码器:

var result User
decoder, err := mapx.NewDecoder(&result, mapx.WithTagName("yaml"))
if err != nil {
return err
}
err = decoder.Decode(data)

解码钩子

mapxNewDecoder 内置注册了一整套 decode hook,使得来自 JSON、表单、环境变量的纯字符串 map 可以直接解码进类型化结构体:

  • time.Time —— 解析 "2006-01-02 15:04:05"(Go 的 time.DateTime 布局)
  • time.Location —— 解析 IANA 名称(例如 "Asia/Shanghai"
  • time.Duration —— 解析 Go 持续时间字符串(例如 "5m"
  • *url.URL —— 解析 URL
  • net.IP / net.IPNet / netip.Addr / netip.AddrPort / netip.Prefix
  • json.RawMessage —— 将源值 marshal 成 JSON bytes
  • *multipart.FileHeader —— 源是长度为 1 的 []*multipart.FileHeader 时取唯一一项
  • collections.Set / SortedSet / ConcurrentSet / ConcurrentSortedSet —— 把 slice 或 array 转为对应的集合类型
  • encoding.TextUnmarshaler —— 任何实现了 UnmarshalText 的类型
  • string → 基础类型的隐式转换(int / uint / float / bool)

collection-set 解码为 string、全部有符号/无符号整数宽度以及 float32/float64 注册。它会拒绝 nil element、string/numeric family mismatch、 numeric overflow、fractional float 转 integer set、NaN 或 infinity 转 integer set,以及负数转 unsigned set。

WithDecodeHook(myHook) 会替换默认 composed hook。若要扩展默认行为,请先把 自定义 hook 和 mapx.DecoderHook compose,再传给 WithDecodeHook

默认解码器行为

NewDecoder 创建的解码器在默认 hook 之外还有以下 VEF 默认值:

默认项作用
TagNamejson读取 json tag,而非上游默认的 mapstructure
Squashtrue启用 squashing。
SquashTagOptioninlinestruct tag 中的 inline 选项标记嵌入字段可展开。
MatchNamemapKey == lo.CamelCase(fieldName)区分大小写的 CamelCase 匹配。

因此,嵌入结构体可以通过 inline tag option 展开:

type Embedded struct {
ID int `json:"id"`
}

type Parent struct {
Name string `json:"name"`
Embedded `json:"embedded,inline"`
}

out, err := mapx.FromMap[Parent](map[string]any{"name": "vef", "id": 1})
// out.ID == 1, out.Name == "vef"

json.Number 处理

默认 DecoderHook 会规范化 number-preserving JSON 解析产生的 json.Numberjson.Decoder.UseNumber):

  • 类型化的数值目标(intuintfloat 及命名数值类型)会进行精确数字解析; 小数或超出目标范围的值会返回 ErrJSONNumberNotIntegerErrJSONNumberOverflow
  • json.Numberjson.RawMessage 目标保留字面数字。
  • any / interface{} 目标以及 map[string]any[]any 容器会看到 float64, 保持 json.Number 之前对动态消费者的运行时契约。

这意味着用 mapx 解码后的 map[string]any 不会再把 json.Number 泄漏到 无类型字段中,而类型化字段即使在 2^53 以上仍能保持精确精度。

错误哨兵

错误含义
ErrInvalidToMapValueToMap 收到的不是 struct
ErrInvalidFromMapTypeFromMap[T]T 不是 struct
ErrCollectionSetNilElementnil 元素不能插入 collection set
ErrCollectionSetIncompatibleKind源值 kind 与 set 元素 kind 不匹配
ErrCollectionSetOverflow数值源会溢出目标 set 元素类型
ErrCollectionSetNonInteger带小数的 float 解码到整数 set 会丢失信息
ErrCollectionSetNotFiniteNaN 或 infinity 不能解码到整数 set
ErrCollectionSetNegative负数不能解码到无符号 set 元素
ErrCollectionSetUnsupportedTarget目标 set 元素 kind 没有转换策略
ErrJSONNumberNotInteger小数或指数形式的 json.Number 不能转成整数目标
ErrJSONNumberOverflowjson.Number 超出目标数值类型范围