
我们聊一个问题为什么 json.Marshal 可以接收任意结构体比如json.Marshal(User{}) json.Marshal(Product{}) json.Marshal([]Order{})json.Marshal在编译时并不知道你会传什么类型。但是它运行时却能知道你传进来的是不是结构体结构体有几个字段字段叫什么字段值是多少字段上有没有jsontag哪些字段要忽略哪些字段空值不输出这背后靠的就是 Go 的反射。一句话先说结论反射就是程序在运行时查看和操作类型信息、字段信息、方法信息和值。它让程序在“不提前知道具体类型”的情况下仍然可以写出通用逻辑。一、什么是反射平时写 Go 代码大多数类型信息在编译期就确定了。例如type User struct { Name string Age int } func PrintUser(u User) { fmt.Println(u.Name) fmt.Println(u.Age) }这里编译器很清楚u 是 User u 有 Name 字段 u 有 Age 字段 Name 是 string Age 是 int所以你可以直接写u.Name u.Age但是如果函数参数是func Print(v any) {}问题就来了v 可能是 User v 可能是 Product v 可能是 []int v 可能是 map[string]string v 也可能是 nil编译期不知道具体类型普通代码就没法直接写v.Name因为any不保证有Name字段。这时就可以用反射在运行时问它你到底是什么类型 你是什么种类 你有哪些字段 字段值是多少 字段上有没有 tag 能不能修改二、Go 反射的两个入口TypeOf 和 ValueOfGo 的反射主要在reflect包里。最常用的两个函数是reflect.TypeOf(v) reflect.ValueOf(v)可以这样理解TypeOf 看类型信息 ValueOf 看值信息完整例子package main import ( fmt reflect ) func main() { var x any 123 t : reflect.TypeOf(x) v : reflect.ValueOf(x) fmt.Println(type:, t) fmt.Println(kind:, t.Kind()) fmt.Println(value:, v) fmt.Println(value kind:, v.Kind()) }输出type: int kind: int value: 123 value kind: int这里的x是一个接口值里面实际装的是int。reflect.TypeOf(x)拿到的是动态类型intreflect.ValueOf(x)拿到的是运行时值123三、Type 和 Kind 有什么区别反射里经常看到两个词Type Kind它们很像但不是一回事。Type是具体类型。Kind是底层分类。看例子package main import ( fmt reflect ) type UserID int func main() { var id UserID 100 t : reflect.TypeOf(id) fmt.Println(type:, t) fmt.Println(name:, t.Name()) fmt.Println(kind:, t.Kind()) }输出type: main.UserID name: UserID kind: int解释一下Type 是 main.UserID Kind 是 int因为UserID是你定义的新类型但它的底层种类是int。再看结构体type User struct { Name string }它的Type 是 main.User Kind 是 struct新手可以这样记Type 更具体你到底叫什么类型 Kind 更粗略你属于哪一类常见 Kind 包括reflect.Bool reflect.Int reflect.String reflect.Struct reflect.Slice reflect.Map reflect.Ptr reflect.Interface四、遍历结构体字段反射最常见的用途之一就是遍历结构体字段。普通代码里如果你知道类型可以直接访问字段user.Name user.Age但如果你写的是通用函数不知道传进来是什么结构体就要用反射。示例package main import ( fmt reflect ) type User struct { Name string Age int } func PrintFields(v any) { rv : reflect.ValueOf(v) rt : reflect.TypeOf(v) if rv.Kind() ! reflect.Struct { fmt.Println(not a struct) return } for i : 0; i rv.NumField(); i { fieldInfo : rt.Field(i) fieldValue : rv.Field(i) fmt.Printf(%s %v\n, fieldInfo.Name, fieldValue) } } func main() { user : User{Name: Tom, Age: 18} PrintFields(user) }输出Name Tom Age 18这里几个 API 很重要rv.NumField()返回结构体有几个字段。rt.Field(i)返回第i个字段的类型信息比如字段名、字段类型、tag。rv.Field(i)返回第i个字段的值。简单理解reflect.Type 负责字段说明书 reflect.Value 负责字段实际值五、struct tag 是什么结构体 tag 是写在字段后面的元信息。例如type User struct { Name string json:name Age int json:age }这里json:name json:age就是 tag。tag 不会直接改变字段本身。它更像写给框架或标准库看的说明这个字段转 JSON 时叫 name 这个字段转 JSON 时叫 age读取 tag 也要用反射。示例package main import ( fmt reflect ) type User struct { Name string json:name validate:required Age int json:age } func main() { t : reflect.TypeOf(User{}) for i : 0; i t.NumField(); i { field : t.Field(i) fmt.Println(field:, field.Name) fmt.Println(json tag:, field.Tag.Get(json)) fmt.Println(validate tag:, field.Tag.Get(validate)) fmt.Println(---) } }输出field: Name json tag: name validate tag: required --- field: Age json tag: age validate tag: ---常见 tag 有json:name yaml:name db:name gorm:column:name validate:required form:name不同库会读取不同 tag。例如encoding/json读取jsonORM 可能读取db、gorm参数校验库可能读取validateWeb 框架可能读取form一句话tag 是结构体字段上的说明书反射是读取说明书的工具。六、类型断言和反射有什么区别你可能会问v.(T)不是也能知道类型吗是但它和反射不是一回事。类型断言适合这种场景我知道它可能是 string帮我确认一下。例如package main import fmt func Print(v any) { if s, ok : v.(string); ok { fmt.Println(string:, s) return } if n, ok : v.(int); ok { fmt.Println(int:, n) return } fmt.Println(unknown) } func main() { Print(hello) Print(100) Print(true) }输出string: hello int: 100 unknown这里你提前写死了string int反射适合这种场景我不知道它是什么结构体但我想遍历它的字段和 tag。对比一下能力类型断言反射是否需要提前知道目标类型需要不需要能否遍历未知结构体字段不适合可以能否读取 struct tag不行可以性能通常更好通常更慢可读性更简单更复杂常见用途判断少数已知类型JSON、ORM、校验器、框架所以优先级一般是能用普通代码就用普通代码 能用接口就用接口 能用类型断言就用类型断言 确实需要通用运行时类型处理再用反射七、反射的三条直觉规则Go 官方博客《The Laws of Reflection》里讲过反射的几个基本规律。新手可以先不用背原文记住这三个直觉1. 从普通值到反射对象v : reflect.ValueOf(x) t : reflect.TypeOf(x)你把普通值交给反射得到反射对象。2. 从反射对象回到普通值x : v.Interface()Interface()可以把reflect.Value重新变成any。然后你可以做类型断言s : x.(string)3. 想修改值必须传指针并且值可设置看例子package main import ( fmt reflect ) func main() { name : Tom v : reflect.ValueOf(name).Elem() if v.CanSet() { v.SetString(Jerry) } fmt.Println(name) }输出Jerry为什么要传name因为如果你只传reflect.ValueOf(name)反射拿到的是一份值不知道原变量在哪里不能修改原变量。传指针后reflect.ValueOf(name).Elem()反射才能找到原变量的位置。一句话反射修改值时要拿到可寻址、可设置的值。八、反射的应用场景反射不是日常业务代码里到处用的东西。它更常出现在框架和通用库里。1. JSON / XML / YAML 序列化例如json.Marshal(user)encoding/json要在运行时读取结构体字段、字段值和jsontag。2. ORM 数据库映射例如type User struct { ID int db:id Name string db:name }ORM 可以通过反射知道User.ID 对应数据库 id 字段 User.Name 对应数据库 name 字段3. 参数校验例如type RegisterRequest struct { Email string validate:required,email Age int validate:gte18 }校验库会读取validatetag检查字段是否合法。4. 配置解析例如把配置文件填充到结构体type Config struct { Port int yaml:port Mode string yaml:mode }配置库通过反射知道字段名、字段类型再把文本值转换进去。5. RPC / Web 框架参数绑定Web 框架经常支持func CreateUser(req CreateUserRequest) {}框架要把 HTTP 请求里的 JSON、form、query 参数绑定到结构体字段也需要读取类型和 tag。6. 通用调试工具比如打印任意结构体字段、比较两个值、深拷贝、对象转 map 等。这些工具往往不知道具体类型所以会使用反射。九、反射的缺点反射很强但不要滥用。主要缺点有1. 代码复杂普通代码user.Name反射代码v.FieldByName(Name)后者更绕也更容易写错。2. 运行时才发现问题普通代码字段写错编译器会报错。反射代码字段名写错可能运行时才发现。3. 性能更差反射需要运行时检查类型和值通常比直接访问字段慢。对于普通业务逻辑不要为了“高级”而使用反射。4. 容易 panic例如v.Field(100) v.SetString(x)如果字段不存在或者值不可设置就可能 panic。所以反射代码经常要检查Kind() IsValid() CanSet() CanInterface() IsNil()十、为什么 JSON 序列化需要反射现在回到核心问题json.Marshal(v any)它的参数是any。这表示你可以传任意类型json.Marshal(User{}) json.Marshal(Product{}) json.Marshal([]int{1, 2, 3}) json.Marshal(map[string]string{name: Tom})encoding/json在编译期不知道你会传什么。所以它必须在运行时做类似这些事1. 判断传进来的是 struct、slice、map、string、int 还是 bool 2. 如果是 struct就遍历字段 3. 读取字段值 4. 读取 json tag 5. 忽略 json:- 的字段 6. 处理 json:name,omitempty 7. 递归处理嵌套结构 8. 生成 JSON 字符串例如type User struct { Name string json:name Age int json:age Password string json:- }json.Marshal要知道Name 字段输出成 name Age 字段输出成 age Password 字段忽略这些都来自结构体字段和 tag。普通代码不知道未知结构体有哪些字段只能靠反射。十一、标准库 json.Marshal 示例先看标准库自己的行为package main import ( encoding/json fmt ) type User struct { Name string json:name Age int json:age Email string json:email,omitempty Password string json:- } func main() { user : User{ Name: Tom, Age: 18, Email: , Password: secret, } data, err : json.Marshal(user) if err ! nil { fmt.Println(marshal error:, err) return } fmt.Println(string(data)) }输出{name:Tom,age:18}为什么没有Email因为json:email,omitemptyEmail是空字符串omitempty表示空值不输出。为什么没有Password因为json:-表示忽略这个字段。十二、手写一个迷你版 JSON 序列化器下面我们手写一个简化版SimpleMarshal。它支持structpointerstringintboolslice / arraymap[string]Tjson:namejson:-omitempty它不是为了替代标准库而是为了理解反射在 JSON 序列化里做了什么。完整代码package main import ( fmt reflect sort strconv strings ) type User struct { Name string json:name Age int json:age Email string json:email,omitempty Password string json:- Active bool json:active Tags []string json:tags,omitempty } func SimpleMarshal(v any) (string, error) { return marshalValue(reflect.ValueOf(v)) } func marshalValue(v reflect.Value) (string, error) { if !v.IsValid() { return null, nil } if v.Kind() reflect.Interface { if v.IsNil() { return null, nil } return marshalValue(v.Elem()) } if v.Kind() reflect.Ptr { if v.IsNil() { return null, nil } return marshalValue(v.Elem()) } switch v.Kind() { case reflect.Struct: return marshalStruct(v) case reflect.String: return strconv.Quote(v.String()), nil case reflect.Bool: if v.Bool() { return true, nil } return false, nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(v.Int(), 10), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return strconv.FormatUint(v.Uint(), 10), nil case reflect.Slice, reflect.Array: return marshalSlice(v) case reflect.Map: return marshalMap(v) default: return , fmt.Errorf(unsupported kind: %s, v.Kind()) } } func marshalStruct(v reflect.Value) (string, error) { t : v.Type() parts : make([]string, 0, t.NumField()) for i : 0; i t.NumField(); i { field : t.Field(i) value : v.Field(i) // PkgPath 不为空表示非导出字段。 // 非导出字段不能安全地通过 Interface 读取JSON 也不会导出它。 if field.PkgPath ! { continue } tag : field.Tag.Get(json) if tag - { continue } name, omitempty : parseJSONTag(tag) if name { name field.Name } if omitempty value.IsZero() { continue } encodedValue, err : marshalValue(value) if err ! nil { return , fmt.Errorf(marshal field %s: %w, field.Name, err) } parts append(parts, strconv.Quote(name):encodedValue) } return { strings.Join(parts, ,) }, nil } func marshalSlice(v reflect.Value) (string, error) { parts : make([]string, 0, v.Len()) for i : 0; i v.Len(); i { encodedValue, err : marshalValue(v.Index(i)) if err ! nil { return , err } parts append(parts, encodedValue) } return [ strings.Join(parts, ,) ], nil } func marshalMap(v reflect.Value) (string, error) { if v.Type().Key().Kind() ! reflect.String { return , fmt.Errorf(only map with string keys is supported) } keys : make([]string, 0, v.Len()) for _, key : range v.MapKeys() { keys append(keys, key.String()) } sort.Strings(keys) parts : make([]string, 0, len(keys)) for _, key : range keys { value : v.MapIndex(reflect.ValueOf(key)) encodedValue, err : marshalValue(value) if err ! nil { return , err } parts append(parts, strconv.Quote(key):encodedValue) } return { strings.Join(parts, ,) }, nil } func parseJSONTag(tag string) (name string, omitempty bool) { if tag { return , false } items : strings.Split(tag, ,) name items[0] for _, item : range items[1:] { if item omitempty { omitempty true } } return name, omitempty } func main() { user : User{ Name: Tom, Age: 18, Email: , Password: secret, Active: true, Tags: []string{go, reflect}, } text, err : SimpleMarshal(user) if err ! nil { fmt.Println(marshal error:, err) return } fmt.Println(text) more, err : SimpleMarshal(map[string]any{ name: Jerry, age: 20, }) if err ! nil { fmt.Println(marshal map error:, err) return } fmt.Println(more) }输出{name:Tom,age:18,active:true,tags:[go,reflect]} {age:20,name:Jerry}十三、逐段拆解这个序列化器入口函数很短func SimpleMarshal(v any) (string, error) { return marshalValue(reflect.ValueOf(v)) }它把任意值转成reflect.Value交给marshalValue。marshalValue做的事情是按种类分发switch v.Kind() { case reflect.Struct: return marshalStruct(v) case reflect.String: return strconv.Quote(v.String()), nil case reflect.Bool: // ... case reflect.Slice, reflect.Array: return marshalSlice(v) case reflect.Map: return marshalMap(v) }这就是 JSON 序列化的核心思路先判断值的种类再按不同种类编码。处理指针if v.Kind() reflect.Ptr { if v.IsNil() { return null, nil } return marshalValue(v.Elem()) }如果传进来的是User{Name: Tom}Kind()会是ptr要用v.Elem()拿到指针指向的结构体。处理结构体结构体序列化的关键是t : v.Type() for i : 0; i t.NumField(); i { field : t.Field(i) value : v.Field(i) }field是字段元信息字段名 字段类型 字段 tag 是否导出value是字段值Tom 18 true读取 tagtag : field.Tag.Get(json)忽略字段if tag - { continue }解析omitemptyname, omitempty : parseJSONTag(tag) if omitempty value.IsZero() { continue }字段名处理if name { name field.Name }最后拼成name:Tom为什么跳过非导出字段Go 里小写字段是非导出的type User struct { Name string age int }标准库 JSON 不会导出小写字段。反射里可以用field.PkgPath ! 判断字段是否非导出。所以上面的代码写了if field.PkgPath ! { continue }十四、真实 encoding/json 比这个复杂得多我们写的SimpleMarshal只是教学版。真实的encoding/json要处理更多情况floatnil slicenil map嵌套结构体匿名字段字段冲突HTML 转义json.Marshalerencoding.TextMarshalermap key 排序循环引用检测更完整的 tag 规则错误类型和边界情况所以不要在生产环境使用这个教学版。它的价值是帮助你理解JSON 序列化器为什么能处理任意结构体。原因就是反射可以在运行时检查值的类型、字段、tag 和字段值。十五、什么时候不要用反射如果你明确知道类型直接写普通代码。不需要反射func PrintUser(user User) { fmt.Println(user.Name) }不需要写成func PrintUser(user User) { v : reflect.ValueOf(user) fmt.Println(v.FieldByName(Name)) }如果你只是想支持多态优先用接口type Writer interface { Write([]byte) (int, error) }如果你只是关心少数几个类型优先用类型断言或 type switch。反射适合你写的是通用框架或库 你不知道调用方会传什么结构体 你需要读取字段和 tag 你需要按类型动态处理值十六、新手学习路线你可以按这个顺序练习反射用reflect.TypeOf打印变量类型。用reflect.ValueOf打印变量值。区分Type和Kind。遍历结构体字段。读取 struct tag。处理指针Kind() reflect.Ptr和Elem()。用CanSet修改一个变量。写一个StructToMap小工具。写一个迷你版 JSON 序列化器。再去看encoding/json的真实行为。反射不需要一开始就学得很深。你先理解Type 看类型 Value 看值 Kind 看种类 StructField 看字段说明 Tag 看字段标签就能读懂很多框架代码了。总结Go 反射可以概括成几句话反射让程序在运行时检查类型和值。reflect.TypeOf获取类型信息。reflect.ValueOf获取值信息。Type是具体类型Kind是底层分类。结构体字段和 struct tag 可以通过反射读取。JSON、ORM、参数校验、配置解析、Web 框架经常使用反射。类型断言适合已知类型反射适合未知结构的通用处理。反射更灵活但也更复杂、更慢、更容易 panic。encoding/json能处理任意结构体本质上就是用反射读取字段、字段值和jsontag。最后记住一句反射不是日常业务代码的优先选择而是编写通用框架和通用工具时的能力。理解反射之后你再看json.Marshal、ORM、validate、配置解析就不会觉得它们像魔法了。参考资料Package reflectThe Laws of ReflectionPackage encoding/jsonGo Wiki: Well-known struct tagsPackage encoding