reflect.Value 与动态值操作

reflect.Value 与动态值操作

一、reflect.Value ------ 值的运行时容器

如果说 reflect.Type 描述了"这是什么类型",那 reflect.Value 就回答了"里面存着什么值"。它是反射体系中操作数据的核心工具。

1.1 基本创建与读取

reflect.ValueOf(i interface{}) 是创建 Value 的唯一入口。传进去的值会被拷贝,所以得到的 Value 是原始值的不可变副本

go 复制代码
var x int = 42
v := reflect.ValueOf(x)

fmt.Println("类型:", v.Type())   // int
fmt.Println("种类:", v.Kind())    // int
fmt.Println("值:",   v.Int())     // 42

Value 提供了一组按 Kind 分类的方法来提取具体值:

Kind 提取方法 说明
Int/Int8...Int64 Int() int64 所有整数类型统一返回 int64
Uint/Uint8...Uint64 Uint() uint64 所有无符号类型统一返回 uint64
Float32/Float64 Float() float64 统一返回 float64
Bool Bool() bool
String String() string
Ptr Elem() Value 解引用,获取指针指向的值

还有一个万能提取方法 Interface() interface{}------把 Value 还原成原始的 interface{} 值:

go 复制代码
v := reflect.ValueOf("hello")
s := v.Interface().(string) // 类型断言还原
fmt.Println(s) // hello

1.2 Kind 决定可用方法

调用与 Kind 不匹配的方法会 panic。例如对字符串调用 Int()

go 复制代码
v := reflect.ValueOf("hello")
v.Int() // panic: reflect: call of reflect.Value.Int on string Value

正确的做法是先用 Kind() 判断,再调用对应方法。写一个通用打印函数就能体现这个逻辑:

go 复制代码
func printValue(v reflect.Value) {
    switch v.Kind() {
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        fmt.Printf("int: %d\n", v.Int())
    case reflect.Float32, reflect.Float64:
        fmt.Printf("float: %f\n", v.Float())
    case reflect.String:
        fmt.Printf("string: %s\n", v.String())
    case reflect.Bool:
        fmt.Printf("bool: %t\n", v.Bool())
    case reflect.Ptr:
        fmt.Printf("ptr -> ")
        printValue(v.Elem())
    default:
        fmt.Printf("%v (%s)\n", v.Interface(), v.Kind())
    }
}

二、修改值------CanSet 与 Elem

2.1 为什么不能直接修改?

reflect.ValueOf(x) 传入的是值的副本,不是原始变量本身。所以默认情况下 Value 是不可设置的:

go 复制代码
var x int = 42
v := reflect.ValueOf(x)
fmt.Println(v.CanSet()) // false
v.SetInt(100)           // panic: reflect: call of reflect.Value.SetInt on unaddressable value

2.2 通过指针修改

要修改原始值,必须传入指针,再用 Elem() 解引用拿到可设置的 Value:

go 复制代码
var x int = 42

// 传入指针
p := reflect.ValueOf(&x)
fmt.Println(p.Kind())    // ptr
fmt.Println(p.CanSet())  // false(指针本身不可设置)

// 解引用获取指向的值
v := p.Elem()
fmt.Println(v.Kind())    // int
fmt.Println(v.CanSet())  // true!

// 现在可以修改了
v.SetInt(100)
fmt.Println(x)           // 100

Elem() 的作用:对指针类型,它返回指针指向的值;对接口类型,它返回接口中动态类型的值。这个"解引用"操作是修改值的关键一步。

2.3 Set 系列方法

拿到可设置的 Value 后,按 Kind 选择对应的 Set 方法:

方法 适用 Kind 说明
SetInt(n int64) Int 系列 设置整数
SetUint(n uint64) Uint 系列 设置无符号整数
SetFloat(f float64) Float 系列 设置浮点数
SetBool(b bool) Bool 设置布尔值
SetString(s string) String 设置字符串
Set(x Value) 任何 Kind 用另一个 Value 替换(类型需匹配)

2.4 结构体字段修改

同样的指针+Elem 模式也适用于结构体:

go 复制代码
type Person struct {
    Name string
    Age  int
}

p := Person{Name: "Alice", Age: 25}
vp := reflect.ValueOf(&p).Elem()

// 修改 Name 字段
nameField := vp.FieldByName("Name")
if nameField.CanSet() {
    nameField.SetString("Bob")
}

// 修改 Age 字段
ageField := vp.FieldByName("Age")
if ageField.CanSet() {
    ageField.SetInt(30)
}

fmt.Println(p) // {Bob 30}

未导出字段不可设置 ------即使传入了指针,CanSet() 对未导出字段仍然返回 false:

go 复制代码
type Secret struct {
    Public  string
    private int // 未导出
}

s := Secret{Public: "hello", private: 42}
v := reflect.ValueOf(&s).Elem()
v.FieldByName("private").CanSet() // false
v.FieldByName("private").SetInt(0) // panic!

三、方法调用------Call 与 MethodByName

反射还能动态调用方法:

go 复制代码
type Calculator struct{}

func (c Calculator) Add(a, b int) int { return a + b }
func (c *Calculator) Multiply(a, b int) int { return a * b }

func main() {
    c := Calculator{}
    v := reflect.ValueOf(c)

    // 调用 Add 方法
    addMethod := v.MethodByName("Add")
    args := []reflect.Value{
        reflect.ValueOf(3),
        reflect.ValueOf(5),
    }
    results := addMethod.Call(args)
    fmt.Println("Add(3,5) =", results[0].Int()) // 8

    // 调用指针接收器方法------需要传入指针
    vp := reflect.ValueOf(&c)
    mulMethod := vp.MethodByName("Multiply")
    results = mulMethod.Call(args)
    fmt.Println("Multiply(3,5) =", results[0].Int()) // 15
}

MethodByName 返回一个 reflect.Value,其 Kind 为 reflect.FuncCall 接收参数列表([]reflect.Value),返回结果列表(也是 []reflect.Value)。

四、切片与 Map 操作

Value 对复合类型也提供了操作方法:

go 复制代码
// 切片操作
nums := []int{10, 20, 30, 40}
v := reflect.ValueOf(&nums).Elem()

fmt.Println("长度:", v.Len())          // 4
fmt.Println("第2个:", v.Index(2).Int()) // 30

// 追加(Set 到新切片)
newSlice := reflect.Append(v, reflect.ValueOf(50))
v.Set(newSlice)
fmt.Println(nums) // [10 20 30 40 50]

// Map 操作
scores := map[string]int{"math": 90, "english": 85}
mv := reflect.ValueOf(&scores).Elem()

fmt.Println("长度:", mv.Len())                 // 2
fmt.Println("math:", mv.MapIndex(reflect.ValueOf("math")).Int()) // 90

// 设置新键值
mv.SetMapIndex(reflect.ValueOf("physics"), reflect.ValueOf(78))
fmt.Println(scores) // map[english:85 math:90 physics:78]

五、练习代码

go 复制代码
// reflect_value_practice.go
package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name   string
    Age    int
    Scores []float64
}

func (s Student) Greet() string {
    return fmt.Sprintf("我是%s,今年%d岁", s.Name, s.Age)
}

func (s *Student) AddScore(score float64) {
    s.Scores = append(s.Scores, score)
}

func main() {
    // 练习1: 基本值读取
    fmt.Println("=== 练习1: 基本值读取 ===")
    readValues()

    // 练习2: 通过指针修改值
    fmt.Println("\n=== 练习2: 通过指针修改值 ===")
    modifyViaPointer()

    // 练习3: 结构体字段修改
    fmt.Println("\n=== 练习3: 结构体字段修改 ===")
    modifyStruct()

    // 练习4: 方法动态调用
    fmt.Println("\n=== 练习4: 方法动态调用 ===")
    callMethods()

    // 练习5: 切片和 Map 反射操作
    fmt.Println("\n=== 练习5: 切片和 Map 反射操作 ===")
    compositeOps()
}

func readValues() {
    values := []interface{}{42, 3.14, "hello", true}
    for _, val := range values {
        v := reflect.ValueOf(val)
        switch v.Kind() {
        case reflect.Int:
            fmt.Printf("  int: %d\n", v.Int())
        case reflect.Float64:
            fmt.Printf("  float: %.2f\n", v.Float())
        case reflect.String:
            fmt.Printf("  string: %s\n", v.String())
        case reflect.Bool:
            fmt.Printf("  bool: %t\n", v.Bool())
        }
    }
}

func modifyViaPointer() {
    var x int = 10
    v := reflect.ValueOf(&x).Elem()
    fmt.Printf("  原值: %d, CanSet: %t\n", x, v.CanSet())
    v.SetInt(99)
    fmt.Printf("  新值: %d\n", x)
}

func modifyStruct() {
    s := Student{Name: "小明", Age: 18, Scores: []float64{90.5, 88.0}}
    v := reflect.ValueOf(&s).Elem()

    nameF := v.FieldByName("Name")
    ageF := v.FieldByName("Age")
    fmt.Printf("  CanSet Name: %t, CanSet Age: %t\n", nameF.CanSet(), ageF.CanSet())

    nameF.SetString("小红")
    ageF.SetInt(20)
    fmt.Printf("  修改后: %+v\n", s)
}

func callMethods() {
    s := Student{Name: "小刚", Age: 17}

    // 值接收器方法
    vs := reflect.ValueOf(s)
    greet := vs.MethodByName("Greet")
    results := greet.Call(nil) // 无参数
    fmt.Printf("  Greet: %s\n", results[0].String())

    // 指针接收器方法
    vps := reflect.ValueOf(&s)
    addScore := vps.MethodByName("AddScore")
    addScore.Call([]reflect.Value{reflect.ValueOf(95.5)})
    fmt.Printf("  添加分数后: %+v\n", s)
}

func compositeOps() {
    // 切片操作
    nums := []int{1, 2, 3}
    sv := reflect.ValueOf(&nums).Elem()
    fmt.Printf("  切片长度: %d, 第2元素: %d\n", sv.Len(), sv.Index(1).Int())

    newSlice := reflect.Append(sv, reflect.ValueOf(4))
    sv.Set(newSlice)
    fmt.Printf("  append后: %v\n", nums)

    // Map 操作
    m := map[string]string{"a": "apple", "b": "banana"}
    mv := reflect.ValueOf(&m).Elem()
    fmt.Printf("  Map长度: %d\n", mv.Len())
    fmt.Printf("  key=a: %s\n", mv.MapIndex(reflect.ValueOf("a")).String())

    mv.SetMapIndex(reflect.ValueOf("c"), reflect.ValueOf("cherry"))
    fmt.Printf("  设置c后: %v\n", m)
}

运行结果

yaml 复制代码
=== 练习1: 基本值读取 ===
  int: 42
  float: 3.14
  string: hello
  bool: true

=== 练习2: 通过指针修改值 ===
  原值: 10, CanSet: true
  新值: 99

=== 练习3: 结构体字段修改 ===
  CanSet Name: true, CanSet Age: true
  修改后: {Name:小红 Age:20 Scores:[90.5 88]}

=== 练习4: 方法动态调用 ===
  Greet: 我是小刚,今年17岁
  添加分数后: {Name:小刚 Age:17 Scores:[90.5 88 95.5]}

=== 练习5: 切片和 Map 反射操作 ===
  切片长度: 3, 第2元素: 2
  append后: [1 2 3 4]
  Map长度: 2
  key=a: apple
  设置c后: map[a:apple b:banana c:cherry]
相关推荐
IT_陈寒4 小时前
Redis的DEL命令居然没删干净数据?这个坑我爬了半天
前端·人工智能·后端
罗超驿5 小时前
SpringMVC 请求参数接收详解|单个参数、多参数、对象参数、@RequestParam参数重命名
java·后端·postman·javaee
__zRainy__5 小时前
Node系列 · ORM:Sequelize 简介
数据库·后端·node.js
AskHarries6 小时前
OpenClaw 如何查看历史对话和响应时间?
后端
n8n6 小时前
Spring Boot 3.2+ 新特性、Spring AI 核心架构与 API Key 安全管理技术文档
后端
fireworks996 小时前
接口鉴权(401与403)
java·后端
n8n6 小时前
Java 21 虚拟线程(Virtual Threads)
后端
Csvn7 小时前
🐍 Day 5: Python 函数详解 — 参数、作用域与一等公民
人工智能·后端
wno7047 小时前
Spring Boot中使用Servlet
spring boot·后端·servlet
老郑聊AI业财智造7 小时前
Spring AI 技术架构与源码分析
java·人工智能·后端·spring·架构·软件工程