← 反射概览 | Type 和 Value →

结构体反射 - 运行时结构体操作

结构体反射允许在运行时检查和操作结构体字段、标签和方法。这是实现 ORM、序列化、验证等框架的基础技术。

📌 核心概念

🏗️

字段访问

NumField/Field

reflect.Value
🏷️

Struct Tag

标签解析

Tag.Get()
✏️

字段修改

可寻址值

CanSet()
🔍

类型检查

Kind/Type

reflect.Kind

字段访问

📝 访问结构体字段

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    City string `json:"city,omitempty"`
}

func main() {
    p := Person{Name: "Alice", Age: 30, City: "Beijing"}
    
    // 获取 reflect.Value
    v := reflect.ValueOf(p)
    
    // 字段数量
    fmt.Printf("NumField: %d\n", v.NumField())
    
    // 访问字段
    for i := 0; i < v.NumField(); i++ {
        field := v.Field(i)
        fmt.Printf("Field %d: %v = %v\n", i, 
            v.Type().Field(i).Name, field.Interface())
    }
    
    // 通过字段名访问
    nameVal := v.FieldByName("Name")
    fmt.Printf("Name: %v\n", nameVal.String())
    
    ageVal := v.FieldByName("Age")
    fmt.Printf("Age: %v\n", ageVal.Int())
}

⚠️ 字段访问注意

  • 导出字段: 只能访问大写开头的导出字段
  • 指针解引用: 如果是指针需要先 Elem()
  • CanSet: 修改前检查字段是否可设置

Struct Tag 解析

📝 解析 Struct Tag

package main

import (
    "fmt"
    "reflect"
    "strings"
)

type User struct {
    ID       int    `json:"id" db:"user_id" validate:"required"`
    Username string `json:"username" db:"username" validate:"min=3"`
    Email    string `json:"email" db:"email" validate:"email"`
    Password string `json:"-" db:"password"`
}

// 解析单个 Tag
func parseSingleTag() {
    var u User
    t := reflect.TypeOf(u)
    
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        tag := field.Tag
        
        fmt.Printf("Field: %s\n", field.Name)
        fmt.Printf("  JSON: %s\n", tag.Get("json"))
        fmt.Printf("  DB: %s\n", tag.Get("db"))
        fmt.Printf("  Validate: %s\n", tag.Get("validate"))
        fmt.Println()
    }
}

// 解析 Tag 选项 (如 omitempty)
func parseTagOptions(tagName string) (string, []string) {
    parts := strings.Split(tagName, ",")
    if len(parts) == 0 {
        return "-", nil
    }
    return parts[0], parts[1:]
}

func main() {
    parseSingleTag()
    
    // 解析选项
    name, opts := parseTagOptions("username,omitempty")
    fmt.Printf("Name: %s, Options: %v\n", name, opts)
}

修改字段值

📝 通过反射修改字段

package main

import (
    "fmt"
    "reflect"
)

type Config struct {
    Host string
    Port int
}

func main() {
    cfg := Config{Host: "localhost", Port: 8080}
    
    // ❌ 错误:不能修改非指针的值
    v := reflect.ValueOf(cfg)
    fmt.Printf("CanSet: %v\n", v.Field(0).CanSet()) // false
    
    // ✅ 正确:使用指针
    v = reflect.ValueOf(&cfg).Elem() // Elem() 解引用
    
    // 修改字段
    if v.Field(0).CanSet() {
        v.Field(0).SetString("127.0.0.1")
    }
    
    if v.Field(1).CanSet() {
        v.Field(1).SetInt(9000)
    }
    
    fmt.Printf("Config: %+v\n", cfg)
    // Config: {Host:127.0.0.1 Port:9000}
}

// 通用字段设置函数
func SetField(obj interface{}, fieldName string, value interface{}) error {
    v := reflect.ValueOf(obj)
    
    if v.Kind() != reflect.Ptr {
        return fmt.Errorf("obj must be a pointer")
    }
    
    v = v.Elem()
    field := v.FieldByName(fieldName)
    
    if !field.CanSet() {
        return fmt.Errorf("field %s cannot be set", fieldName)
    }
    
    field.Set(reflect.ValueOf(value))
    return nil
}

实用模式

1. JSON 序列化

📝 简化版 JSON 序列化

func ToJSON(v interface{}) string {
    val := reflect.ValueOf(v)
    if val.Kind() == reflect.Ptr {
        val = val.Elem()
    }
    
    var parts []string
    for i := 0; i < val.NumField(); i++ {
        field := val.Type().Field(i)
        tag := field.Tag.Get("json")
        
        if tag == "-" {
            continue
        }
        
        name := field.Name
        if tag != "" {
            name = strings.Split(tag, ",")[0]
        }
        
        value := val.Field(i)
        parts = append(parts, fmt.Sprintf(`"%s":%v`, name, value.Interface()))
    }
    
    return "{" + strings.Join(parts, ",") + "}"
}

2. 数据库映射

📝 结构体到数据库列映射

func GetColumnNames(v interface{}) []string {
    t := reflect.TypeOf(v)
    if t.Kind() == reflect.Ptr {
        t = t.Elem()
    }
    
    var columns []string
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        dbTag := field.Tag.Get("db")
        
        if dbTag == "-" {
            continue
        }
        
        if dbTag != "" {
            columns = append(columns, dbTag)
        } else {
            columns = append(columns, field.Name)
        }
    }
    
    return columns
}

最佳实践

✅ 结构体反射使用建议

  • 指针传递: 修改字段必须传指针
  • CanSet 检查: 修改前检查是否可设置
  • 缓存 Type: 避免重复获取 Type 信息
  • 优先泛型: Go 1.18+ 优先使用泛型