Go 反射和结构体标签入门:看懂 JSON、校验和框架背后的机制

本文通过 JSON 标签和简单 required 校验器讲解 Go 反射的基本概念,帮助初学者理解结构体标签和框架自动绑定背后的机制。

为什么入门阶段也值得理解一点反射

很多 Go 初学者第一次看到反射,并不是因为主动想学它,而是因为结构体字段后面那一串标签:

type User struct {
	ID    int64  `json:"id"`
	Email string `json:"email"`
}

你可能会好奇:encoding/json 为什么知道要把 Email 变成 email?Web 框架为什么能自动把请求体绑定到结构体?校验库为什么能通过 validate:"required" 判断字段必填?这些能力背后都和反射有关。

反射允许程序在运行时观察类型和值。它很强,但也容易让代码变复杂。普通业务逻辑里,你应该优先写明确的结构体、函数和方法,而不是动不动就用反射。但理解反射的基本机制,会让你看标准库和框架时更踏实,也能帮助你判断什么时候该用工具,什么时候自己写几行明确代码更好。

这篇文章不追求把 reflect 包讲全,而是通过两个小例子建立直觉:读取结构体标签,以及写一个非常简单的必填字段校验器。

JSON 标签不是注释

先看一个结构体:

type Article struct {
	ID      int64  `json:"id"`
	Title   string `json:"title"`
	Content string `json:"content,omitempty"`
}

编码:

article := Article{
	ID:    1,
	Title: "Go 反射入门",
}

data, err := json.Marshal(article)
if err != nil {
	return err
}
fmt.Println(string(data))

输出类似:

{"id":1,"title":"Go 反射入门"}

Content 没有输出,因为它是空字符串,并且标签里有 omitempty。这些标签不是普通注释,运行时可以被反射读取。encoding/json 正是通过反射遍历结构体字段,读取 json 标签,再决定字段名和编码策略。

有一个重要前提:字段必须导出,也就是首字母大写。下面这个字段不会被 JSON 包正常编码:

type Article struct {
	title string `json:"title"`
}

标签不能绕过 Go 的可见性规则。小写字段是包内私有,标准库外部包不能直接访问它。

reflect.Type 和 reflect.Value

反射里最常见的两个类型是 reflect.Typereflect.Value。前者描述“类型是什么”,后者描述“值是什么”。

article := Article{ID: 1, Title: "Go"}

t := reflect.TypeOf(article)
v := reflect.ValueOf(article)

fmt.Println(t.Name())     // Article
fmt.Println(t.NumField()) // 3
fmt.Println(v.Field(0))   // 1

遍历字段:

for i := 0; i < t.NumField(); i++ {
	field := t.Field(i)
	value := v.Field(i)
	fmt.Println(field.Name, field.Tag.Get("json"), value)
}

field.Tag.Get("json") 会读取 json 标签。输出可能是:

ID id 1
Title title Go
Content content,omitempty 

这就是很多库读取结构体标签的基本方式。它们拿到字段名、字段类型、标签内容和值,再根据自己的规则处理。

处理指针输入

真实函数里,调用方可能传结构体,也可能传结构体指针。反射时要先处理指针:

func inspect(input interface{}) error {
	v := reflect.ValueOf(input)
	if v.Kind() == reflect.Ptr {
		if v.IsNil() {
			return fmt.Errorf("input is nil")
		}
		v = v.Elem()
	}

	if v.Kind() != reflect.Struct {
		return fmt.Errorf("input must be struct")
	}

	t := v.Type()
	for i := 0; i < v.NumField(); i++ {
		field := t.Field(i)
		fmt.Println(field.Name)
	}
	return nil
}

Kind 表示值的类别,比如结构体、指针、字符串、整数、切片。Elem 可以取得指针指向的值。反射代码里经常要先判断类别,再做下一步,否则很容易 panic。

这也是反射代码比普通代码难写的原因:很多错误从编译期推迟到了运行期。你需要自己做更多检查。

写一个简单 required 校验器

定义请求结构体:

type CreateUserRequest struct {
	Email    string `json:"email" validate:"required"`
	Password string `json:"password" validate:"required"`
	Name     string `json:"name"`
}

实现一个只支持字符串必填的校验器:

func ValidateRequired(input interface{}) error {
	v := reflect.ValueOf(input)
	if v.Kind() == reflect.Ptr {
		if v.IsNil() {
			return fmt.Errorf("input is nil")
		}
		v = v.Elem()
	}

	if v.Kind() != reflect.Struct {
		return fmt.Errorf("input must be struct")
	}

	t := v.Type()
	for i := 0; i < v.NumField(); i++ {
		field := t.Field(i)
		if field.Tag.Get("validate") != "required" {
			continue
		}

		value := v.Field(i)
		if value.Kind() != reflect.String {
			continue
		}

		if strings.TrimSpace(value.String()) == "" {
			name := field.Tag.Get("json")
			if name == "" {
				name = field.Name
			}
			name = strings.Split(name, ",")[0]
			return fmt.Errorf("%s is required", name)
		}
	}

	return nil
}

使用:

req := CreateUserRequest{
	Email: "xiaolin@example.com",
}

if err := ValidateRequired(req); err != nil {
	fmt.Println(err) // password is required
}

这个例子很小,但已经展示了框架校验的大致思路:遍历字段,读取标签,根据字段类型和值执行规则。真正的校验库会支持更多类型、嵌套结构体、切片、错误集合和自定义规则。

什么时候不该用反射

如果你只校验一个明确类型,直接写方法通常更好:

func (r CreateUserRequest) Validate() error {
	if strings.TrimSpace(r.Email) == "" {
		return fmt.Errorf("email is required")
	}
	if strings.TrimSpace(r.Password) == "" {
		return fmt.Errorf("password is required")
	}
	return nil
}

这段代码比反射版本更容易读,也能被编译器检查。反射适合“处理任意结构体”的通用工具,不适合普通业务里为了少写两行判断而引入复杂度。

一个实用判断是:如果你的函数参数类型已经明确,就先不用反射;如果你的函数必须接收许多未知结构体,并根据标签统一处理,反射才可能合适。

小结

反射让 Go 程序能在运行时观察类型、字段、标签和值。encoding/json、Web 框架、配置库、校验库和 ORM 都大量使用这套机制。理解 reflect.Typereflect.ValueKindElem 和结构体标签后,你就能看懂很多“自动绑定”背后的基本原理。

但反射不是日常业务代码的首选。它更难读,运行期错误更多,性能也通常不如直接代码。入门阶段学反射的目的,不是以后到处写反射,而是知道工具为什么能工作,以及什么时候应该保持简单。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页