欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 新车 > GO语言入门经典-反射3(Value 与对象的值)

GO语言入门经典-反射3(Value 与对象的值)

2025/4/19 8:51:03 来源:https://blog.csdn.net/m0_73558214/article/details/147058317  浏览:    关键词:GO语言入门经典-反射3(Value 与对象的值)

12.3 Value 与对象的值

调用 ValueOf 函数,可以获得 Go 代码对象值相关的信息。这些信息将由 Value 结构体封装。例如:

var nm uint32 = 10000
theVal := reflect.ValueOf(nm)

上述代码先定义了变量 nm,类型为 uint32,初始化的值为 10000,接着调用 ValueOf 函数获得一个 Value 对象,最后通过此对象可以在应用程序运行期间进行动态分析,例如:

if theVal.Kind() == reflect.Uint32 {fmt.Printf("此对象的值: %v\n", theVal.Uint())
}

12.3.1 修改对象的值

使用 Value 类型公开的 Set 方法,能在运行时修改某个对象的值。其实现的功能与赋值运算符相同,但通过反射技术进行赋值,在一些需要动态处理的代码逻辑中会比较灵活(例如动态生成函数体逻辑)。

Set 方法是用另一个 Value 对象的值来修改当前 Value 对象的值。在调用 Set 方法前,最好先调用 CanSet 方法,判断对象的值是否允许修改。

请看下面例子:

var who string = "小明"
fmt.Printf("变量 who 的原值: %v\n", who)// 通过反射技术修改变量的值
val := reflect.ValueOf(&who)
if val.Kind() == reflect.String {if val.CanSet() {val.Set(reflect.ValueOf("小吴"))} else {fmt.Println("变量 who 不允许被修改")}
}fmt.Printf("修改后,变量 who 的值: %v\n", who)

上面代码首先定义变量 who,初始值为“小明”,然后通过 ValueOf 函数取得与变量相关的 Value 对象,最后调用 Set 方法尝试将变量 who 值修改为“小吴”。

可是,运行上述代码后,得到的结果是变量 who 不允许被修改。

变量 who 的原值: 小明
变量 who 不允许被修改
修改后,变量 who 的值: 小明

这说明 CanSet 方法返回了 false。此时不妨查看一下 CanSet 方法的源代码。

func (v Value) CanSet() bool {return v.flag&(flagAddr|flagRO) == flagAddr
}

不管 Value 对象所包含的值是否为只读,首先它要满足的条件是——可以引用其内存地址。因为变量在传递过程中会进行自我复制,这会导致后续代码所操作的值已经不是 who 变量自身,而是它的副本。

所以,在调用 ValueOf 函数的时候,应该传递 who 变量的地址。上面代码可以做以下修改。

val := reflect.ValueOf(&who)
// 获取指针指向的对象
val = val.Elem()
if val.Kind() == reflect.String {......
}

注意 ValueOf 函数获取的是 who 变量的地址,即其类型为 *string,它的 Kind 方法返回的不是 string,而是 ptr。所以在修改对象值之前,可以调用一次 Elem 方法,获取另一个 Value 对象,它包含 who 变量的实际值。

代码经过修改后,再次运行就能得到正确的结果。who 变量在传递过程中没有进行自我复制,只是传递了它的内存地址,因此它能够被修改。

变量 who 的原值: 小明
修改后,变量 who 的值: 小吴

为了方便调用,在 Set 方法之外,Value 类型还公开了以下方法:

func (v Value) SetBool(x bool)
func (v Value) SetBytes(x []byte)
func (v Value) SetComplex(x complex128)
func (v Value) SetFloat(x float64)
func (v Value) SetInt(x int64)
func (v Value) SetMapIndex(key, elem Value)
func (v Value) SetPointer(x unsafe.Pointer)
func (v Value) SetString(x string)
func (v Value) SetUint(x uint64)

intint8int16int32int64 类型的值统一使用 SetInt 方法来设置;uintuint8uint16uint32uint64 类型的值统一使用 SetUint 方法来设置;float32float64 类型的值使用 SetFloat 方法来设置;complex64complex128 类型的值使用 SetComplex 方法来设置。

下面例子演示了 SetBool 方法的使用。

var bv = false
fmt.Printf("变量的原值: %v\n", bv)var val = reflect.ValueOf(&bv)
// 获取指针指向的值
var bval = val.Elem()
if bval.Kind() == reflect.Bool && bval.CanSet() {bval.SetBool(true)
}fmt.Printf("变量的最新值: %v\n", bv)

12.3.2 读写结构体实例的字段

Type 类型相似,Value 类型也定义了 NumFieldFieldFieldByName 等方法。用法与 Type 相同,不同的是,在 Type 类型中,这些方法获取的是字段的类型,而在 Value 类型中,这些方法获取的是字段的值。

例如,dog 结构体的定义如下:

type dog struct {Nick stringColor stringAge uint8
}

定义 printValues 函数,用于输出对象中各个字段的值。

func printValues(obj interface{}) {var theVal = reflect.ValueOf(obj)// 如果传递过来的是对象的指针// 那么先获取该指针所指向的对象if theVal.Kind() == reflect.Ptr {theVal = theVal.Elem()}// 获取字段成员数量ln := theVal.NumField()// 访问所有字段for i := 0; i < ln; i++ {tm := theVal.Type().Field(i).Namevm := theVal.Field(i).Interface()fmt.Printf("%s: %v\n", tm, vm)}
}

定义 setValues 函数,用于输出对象中各个字段的值。

func setValue(obj interface{}, fdname string, fdval interface{}) {var objval = reflect.ValueOf(obj)if objval.Kind() != reflect.Ptr {fmt.Println("请使用指针类型")return}objval = objval.Elem()// 查找字段fd := objval.FieldByName(fdname)if fd.IsValid() == false {fmt.Println("未找到目标字段")return}// 验证一下值的类型是否与字段匹配newVal := reflect.ValueOf(fdval)if fd.Kind() != newVal.Kind() {fmt.Println("字段值的类型不匹配")return}// 设置新值fd.Set(newVal)
}

在修改对象的字段成员时,也应该传递对象的内存地址,否则将操作失败。

定义 dog 类型的变量,然后实例化。

var mypet = dog{Nick: "Peter",Color: "black",Age: 2,
}

首先调用 printValues 函数输出 dog 对象的字段值,然后调用 setValue 方法修改 Age 字段的值,最后再次打印 dog 对象的字段值。

fmt.Println("----- 修改前 -----")
printValues(mypet)
setValue(&mypet, "Age", uint8(5))
fmt.Println("\n----- 修改后 -----")
printValues(mypet)

上述例子的运行结果如下:

----- 修改前 -----
Nick: Peter
Color: black
Age: 2
----- 修改后 -----
Nick: Peter
Color: black
Age: 5

12.3.3 更新数组/切片的元素

如果 Value 对象所代表的值是数组/切片类型,就可以使用 Index 方法获取指定索引处的元素的值。Index 方法返回的值也是 Value 类型。

在使用反射技术读写数组/切片的元素时,要注意以下规则:

  1. 指定的索引值不能超出有效范围。索引的有效范围为 [0, n)
  2. 新值的类型必须与旧值匹配。例如,元素的原值为 float32 类型,在更新元素时,如果新值是 string 类型,就会发生冲突。

下面代码定义了 updateElement 函数,它的功能是更新元素值。

func updateElement(obj interface{}, index int, elval interface{}) {v := reflect.ValueOf(obj)// 要求目标对象是数组或者切片if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {// 获取元素的总数量ln := v.Len()// 验证指定的索引是否有效if index < 0 || index >= ln {fmt.Println("索引超出有效范围")return}// 获取元素值oldval := v.Index(index)newval := reflect.ValueOf(elval)// 验证新值的类型是否与旧值匹配if oldval.Kind() != newval.Kind() {fmt.Println("元素值的类型不匹配")return}// 更新元素值oldval.Set(newval)} else {fmt.Println("对象类型不是数组或切片类型")return}
}

接下来可以测试一下updateElement 函数。

var kx = []int{1, 5, 7, 12}
fmt.Println("更新前:", kx)
updateElement(kx, 1, 9000)
fmt.Println("更新后:", kx)

上面代码中,先定义变量 kx,它是切片类型,初始化之后,其中包含 4 个元素。然后调用 updateElement 函数,将索引为 1 的元素更新为 9000。程序的执行结果如下:

更新前: [1 5 7 12]
更新后: [1 9000 7 12]

12.3.4 调用函数

使用 Value.Call 方法可以实现通过反射技术来调用函数,Call 方法的签名如下:

func Call(in []Value) []Value

Call 调用比较简单,in 参数表示目标函数的输入参数列表,调用后 Call 方法会将目标函数的返回值作为结果返回(可理解为输出参数)。

下面的代码定义了一个 callFunc 函数,它可以根据传入的参数来调用不同的函数,并返回调用结果。

func callFunc(fun interface{}, args ...interface{}) []interface{} {fv := reflect.ValueOf(fun)if fv.Kind() != reflect.Func {fmt.Println("被调用的不是函数")return []interface{}{}}// 传入的参数个数inlen := len(args)// 被调用函数的参数个数funptLen := fv.Type().NumIn()// 检查传入参数的个数是否正确if inlen != funptLen {fmt.Println("传入的参数个数不正确")return []interface{}{}}// 检查参数类型是否正确for i := 0; i < inlen; i++ {ti := reflect.TypeOf(args[i])tfi := fv.Type().In(i)if ti.Kind() != tfi.Kind() {fmt.Println("参数类型不正确")return []interface{}{}}}// 调用目标函数var prts = make([]reflect.Value, inlen)for i := 0; i < inlen; i++ {prts[i] = reflect.ValueOf(args[i])}var res = fv.Call(prts)// 提取返回值outlen := len(res)if outlen == 0 {return []interface{}{}}var outs = make([]interface{}, outlen)for i := 0; i < outlen; i++ {outs[i] = res[i].Interface()}return outs
}

定义 addsub 函数,稍后可以通过 callFunc 函数去调用。

func add(m, n int32) int32 {return m + n
}func sub(p, q int32) int32 {return p - q
}

callFunc 函数调用 addsub 函数。

var a1, a2 int32 = 15, 17
var r1 = callFunc(add, a1, a2)
fmt.Printf("add(%v, %v) => %v\n", a1, a2, r1)a1, a2 = 30, 12
var r2 = callFunc(sub, a1, a2)
fmt.Printf("sub(%v, %v) => %v\n", a1, a2, r2)

调用成功后,屏幕将输出以下内容:

add(15, 17) => [32]
sub(30, 12) => [18]

12.3.5 调用方法

运用反射调用方法的过程与调用函数接近,都会用到 Value.Call 方法。与调用函数相比,反射调用方法多了一个步骤——先获取与方法关联的 Value 对象。具体而言,就是:

  1. 获取对象实例的 Value
  2. 使用 MethodMethodByName 方法查找出对象指定方法的 Value
  3. 再调用与方法关联的 Value.Call 方法。

下面通过一个示例来做演示。

步骤 1:定义 Student 结构体,其中有四个字段。
type Student struct {id uintname, city stringcourse string
}
步骤 2:为 Student 结构体定义方法,用于读取和修改字段。
// 读写 id 字段
func (x Student) GetID() uint {return x.id
}
func (x *Student) SetID(id uint) {x.id = id
}// 读写 name 字段
func (x Student) GetName() string {return x.name
}
func (x *Student) SetName(name string) {x.name = name
}// 读写 city 字段
func (x Student) GetCity() string {return x.city
}
func (x *Student) SetCity(city string) {x.city = city
}// 读写 course 字段
func (x Student) GetCourse() string {return x.course
}
func (x *Student) SetCourse(course string) {x.course = course
}
步骤 3:定义变量 obj,类型为 Student
var obj Student
步骤 4:使用反射调用 obj 变量的 SetXXX 方法,以修改各个字段的值。
valOfObj := reflect.ValueOf(&obj)
// 调用 SetID 方法
m := valOfObj.MethodByName("SetID")
p := reflect.ValueOf(uint(187005))
m.Call([]reflect.Value{p})
// 调用 SetName 方法
m = valOfObj.MethodByName("SetName")
p = reflect.ValueOf("小刘")
m.Call([]reflect.Value{p})
// 调用 SetCity 方法
m = valOfObj.MethodByName("SetCity")
p = reflect.ValueOf("珠海")
m.Call([]reflect.Value{p})
// 调用 SetCourse 方法
m = valOfObj.MethodByName("SetCourse")
p = reflect.ValueOf("C++")
m.Call([]reflect.Value{p})

注意,在调用 ValueOf 函数获取 objValue 对象时,应该传递指针类型(即 *Student),因为 SetXXX 方法在定义时指定对象的“接收者”为 *Student。如果不使用指针类型,Value 对象的 MethodByName 方法将找不到 SetXXX 方法。

步骤 5:最后输出 obj 变量各字段的值,以验证修改操作是否成功。
fmt.Println("使用反射设置字段的值后:")
fmt.Printf("id: %v\n", obj.GetID())
fmt.Printf("name: %v\n", obj.GetName())
fmt.Printf("city: %v\n", obj.GetCity())
fmt.Printf("course: %v\n", obj.GetCourse())
步骤 6:运行示例程序,结果如下:
使用反射设置字段的值后:
id: 187005
name: 小刘
city: 珠海
course: C++

12.3.6 读写映射类型的元素

映射类型的元素由 key 和元素值组成,所以 Value.SetMapIndex 方法在调用时需提供两个参数。

func SetMapIndex(key Value, elem Value)

SetMapIndex 方法可以通过反射技术为映射对象设置元素,对应地,MapIndex 方法可以根据给定的 key 返回指定的元素,也可以调用 MapKeys 方法获取映射对象中所有元素的 key

下面的示例演示了 SetMapIndexMapKeysMapIndex 方法的用法。

// 创建映射实例
var mp = make(map[string]float32)
// 获取相关的 Value 对象
valOfMap := reflect.ValueOf(mp)
// 设置映射元素
valOfMap.SetMapIndex(reflect.ValueOf("T1"),reflect.ValueOf(float32(0.0071)),
)
valOfMap.SetMapIndex(reflect.ValueOf("T2"),reflect.ValueOf(float32(9.202)),
)
valOfMap.SetMapIndex(reflect.ValueOf("T3"),reflect.ValueOf(float32(-0.03)),
)
// 获取 key 集合
var keys = valOfMap.MapKeys()
// 输出映射中的元素列表
for _, k := range keys {v := valOfMap.MapIndex(k)fmt.Printf("%v - %v\n", k.Interface(), v.Interface())
}

上述代码首先定义了 mp 变量,调用 make 函数生成一个映射实例,元素的 keystring 类型,元素值为 float32 类型,随后使用反射为其设置三个元素,最后将元素列表读出并输出到屏幕上。输出内容如下:

T3 - -0.03
T1 - 0.0071
T2 - 9.202

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词