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]
相关推荐
jinzhe1 小时前
终端bash&zsh中实现基于前缀的历史命令搜索
后端
用户8181870627461 小时前
二、多线程并发篇
后端·程序员
Zane19941 小时前
ThreadLocal 与 BlockingQueue:线程本地变量的内存泄漏原理,以及阻塞队列怎么选?
java·后端
橘色的喵1 小时前
一块 RISC-V MCU 是怎么启动的: 内存保护、中断与从 Flash 直接执行
后端
长栎1 小时前
JDBC 用了 30 年的 Bridge 模式,多数人以为它只是策略模式换了层皮
后端
Ai拆代码的曹操1 小时前
排查 3 小时,问题竟在 Dubbo 路由规则:一个 force 的坑
后端
无责任此方_修行中2 小时前
换个版本号就能升级?可没那简单:pnpm 11 升级踩坑记
javascript·后端·npm
SelectDB2 小时前
洋钱罐基于 SelectDB 实现 Hive 数据湖透明加速:查询 P95 从 300 秒降至 20 秒的完整实践
后端
Chen_LSN2 小时前
C语言——深度理解指针(4)
后端