Go 反射实战:用 reflect 写通用结构体转 map 与按 tag 校验
test12026-11-031 次阅读
反射能帮我们做什么
Go 是静态语言,大多数时候用接口和泛型就够了。但当你要写序列化、参数绑定、配置解析这类"对任意结构体都通用"的工具时,反射就派上用场。reflect 包能在运行时读出字段名、类型、tag,也能动态读取值。代价是失去编译期检查,且比直接访问慢,所以要把它用在真正需要通用性的地方。
把结构体转成 map
下面这个函数遍历结构体的导出字段,把字段名(或 json tag)当作 key,字段值当作 value,返回一个 map。常用于把请求体转成可打印的日志或转交泛型写入层。
func StructToMap(v interface{}) map[string]interface{} {
out := make(map[string]interface{})
rv := reflect.ValueOf(v)
for rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}
if rv.Kind() != reflect.Struct {
return out
}
rt := rv.Type()
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
if !field.IsExported() {
continue
}
key := field.Tag.Get("json")
if key == "" || key == "-" {
key = field.Name
} else {
key = strings.Split(key, ",")[0]
}
out[key] = rv.Field(i).Interface()
}
return out
}
按 tag 做必填校验
很多 Web 框架的校验器底层就是这个思路。我们定义一个 validate:"required" 的 tag,遍历字段时检查非空,收集所有错误一次返回,而不是遇到第一个就中断。
func Validate(v interface{}) []string {
var errs []string
rv := reflect.ValueOf(v)
for rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}
rt := rv.Type()
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
if field.Tag.Get("validate") != "required" {
continue
}
fv := rv.Field(i)
switch fv.Kind() {
case reflect.String:
if strings.TrimSpace(fv.String()) == "" {
errs = append(errs, field.Name+" 不能为空")
}
case reflect.Int:
if fv.Int() == 0 {
errs = append(errs, field.Name+" 不能为 0")
}
}
}
return errs
}
type User struct {
Name string `json:"name" validate:"required"`
Age int `json:"age" validate:"required"`
Email string `json:"email"`
}
func main() {
u := User{Name: "", Age: 0, Email: "a@b.com"}
if errs := Validate(u); len(errs) > 0 {
fmt.Println("校验失败:", errs)
}
}
性能与使用边界
反射调用比直接字段访问慢一到两个数量级。如果这段代码在热路径上(比如每个请求都跑),可以用代码生成在编译期产出一个类型专属的编码器,或者仅在启动时构建一次字段元数据缓存。经验法则:通用工具层用反射,业务热点路径尽量绕开。
小结
reflect 把"对任意结构体通用"变成可能,典型落点是序列化、校验、配置映射。写反射代码时记得处理指针和未导出字段,并且只在必要的通用层使用,避免把性能损耗带进请求主链路。
T
test1
文章作者
反射能帮我们做什么 Go 是静态语言,大多数时候用接口和泛型就够了。但当你要写序列化、参数绑定、配置解析这类"对任意结...
- 分类
- 技术
- 发布时间
- 2026-11-03
- 字数
- 约 2222 字
- 阅读
- 1 次
