Go基础:利用反射来修改变量的值
共 10083字,需浏览 21分钟
·
2021-06-03 00:14
本文内容:
Go语言反射的可寻址概念
Go语言反射的可修改概念
Go语言反射的使用示例
可寻址
回想一下Go语言的表达式,比如x
,x.f[1]
,*p
这样的表达式表示一个变量,而x+1
,f(2)
之类的表达式则不表示变量。一个变量是一个可寻址的存储区域,包含了一个值,并且可以通过这个地址来更新值。
对reflect.Value
也有一个类似的区分,有些是可寻址的:
x := 2 // 是否可寻址
a := reflect.ValueOf(2) // no
b := reflect.ValueOf(x) // no
c := reflect.ValueOf(&x) // no
d := c.Elem() // yes(x)
a里面的值是不可寻址的,它包含的是整数2的一个副本。
b也是如此。
c里面的值也是不可寻址的,它包含的是指针
&x
的一个副本。通过
reflect.ValueOf(x)
返回的reflect.Value
对象都是不可寻址的。d是通过对c中的指针提领出来的,所以它是可寻址的。
调用
reflect.ValueOf(&x).Elem()
可以获得任意变量x的可寻址的Value值。
可以通过变量的CanAddr()
方法来询问reflect.Value
变量是否可寻址:
fmt.Println(a.CanAddr()) // false
fmt.Println(b.CanAddr()) // false
fmt.Println(c.CanAddr()) // false
fmt.Println(d.CanAddr()) // true
我们可以通过一个指针来间接获取一个可寻址的reflect.Value
对象,即使这个指针本身是不可寻址的。
可寻址的常见规则在反射包的CanAddr()
方法的注释上有清晰的说明:
切片成员(an element of a slice)
可寻址数组成员(an element of an addressable array)
可寻址结构体的字段(a field of an addressable struct)
解析指针的内容(the result of dereferencing a pointer)
从一个可寻址的reflect.Value()
获取变量需要三步:
调用
Addr()
,返回一个Value
,其中包含一个指向变量的指针。在这个
Value
上调用Interface()
,得到一个包含这个指针的interface{}
值。最后,如果我们知道变量类型,使用类型断言将接口内容转换成一个普通指针。
之后就可以通过这个指针来更新变量了。
Set()
除了通过指针来更新变量之外,还可以直接调用reflect.Value.Set()
方法来更新变量。
d.Set(reflect.ValueOf(4))
fmt.Println(x) // "4"
一般由编译器在编译期检查可赋值性条件,在这种情况下则是在运行时由Set()
方法来检查。上面的变量和值都是int
类型,但如果变量类型是int64
,这个程序就会崩溃。
// 崩溃:int64不可被赋值给int
d.Set(reflect.ValueOf(int64(5)))
当然,在一个不可寻址的reflect.Value
上调用Set()
方法也会崩溃:
// 崩溃:在不可寻址的值上使用Set()
b.Set(reflect.ValueOf(3))
除了基本的Set()
方法之外,还有一些衍生函数:SetInt()
,SetUint()
,SetString()
,SetFloat()
等
这些方法还有一定程度的容错性。只要变量类型是某种带符号的整数,比如SetInt()
,甚至可以是底层类型是带符号整数的命名类型,都可以成功。如果值太大了还会无提示地截断它。但需要注意的是,在指向interface{}
变量的reflect.Value
上调用SetInt()
会崩溃,调用Set()
反而没有问题。
x := 1
rx := reflect.ValueOf(&x).Elem()
rx.SetInt(2) // OK,x = 2
rx.Set(reflect.ValueOf(3)) // OK,x = 3
rx.SetString("hello") // 崩溃:字符串不能赋给整数
rx.Set(reflect.ValueOf("hello")) // 崩溃:字符串不能赋给整数
var y interface{}
ry := reflect.ValueOf(&y).Elem()
ry.SetInt(2) // 崩溃:在指向接口的Value上调用SetInt
ry.Set(reflect.ValueOf(3)) // OK,y = int(3)
ry.SetString("hello") // 崩溃:在指向接口的Value上调用SetString
ry.Set(reflect.ValueOf("hello")) // OK,y = "hello"
可修改
反射可以读取到未导出结构字段的值,但不能更新这些值。
一个可寻址的reflect.Value
会记录它是否通过遍历一个未导出字段来获得,如果是通过未导出字段来获得的,则不允许被修改。所以在更新变量前用CanAddr()
来检查并不能保证正确。CanSet()
方法才能正确地报告一个reflect.Value
是否可寻址且可更改。
使用示例
访问结构体字段标签
显示类型的方法
访问结构体字段标签
在学习了反射的基本使用之后,我们编写一个示例程序。
在一个Web服务器中,绝大部分HTTP处理函数的第一件事就是提取请求参数到局部变量中。我们将定义一个工具函数Unpack()
,使用结构体字段标签来简化HTTP处理程序的编写。
首先,我们展示如何使用这个方法。下面的search()
函数就是一个HTTP处理函数,它定义一个变量data,data的类型是一个字段与HTTP请求参数对应的匿名结构。结构体的字段标签指定参数名称,这些名称一般比较短,含义也比较模糊,毕竟URL长度有限,不能浪费。Unpack函数从请求中提取数据来填充这个结构体,这样不仅可以方便地访问,还避免了手动转换类型。
func search(resp http.ResponseWriter, req *http.Request) {
var data struct{
Labels []string `http:"1"`
MaxResults int `http:"max"`
Exact bool `http:"x"`
}
data.MaxResults = 10
if err := Unpack(req, &data); err != nil {
http.Error(resp, err.Error(), http.StatusBadRequest) // 400
return
}
}
我们将重点放在Unpack()
函数的实现上:
func Unpack(req *http.Request, ptr interface{}) error {
if err := req.ParseForm(); err != nil {
return err
}
// 创建字段映射表,键为有效名称,值为字段名称
fields := make(map[string]reflect.Value)
v := reflect.ValueOf(ptr).Elem()
for i := 0; i < v.NumField(); i++ {
fieldInfo := v.Type().Field(i)
tag := fieldInfo.Tag
name := tag.Get("http")
if name == "" {
name = strings.ToLower(fieldInfo.Name)
}
fields[name] = v.Field(i)
}
// 对请求中每个参数更新结构体中对应的字段
for name, values := range req.Form {
f := fields[name]
if !f.IsValid() {
continue // 忽略不能识别的HTTP参数
}
for _, value := range values {
if f.Kind() == reflect.Slice {
elem := reflect.New(f.Type().Elem()).Elem()
if err := populate(elem, value); err != nil {
return fmt.Errorf("%s : %v", name, err)
}
f.Set(reflect.Append(f, elem))
} else {
if err := populate(f, value); err != nil {
return fmt.Errorf("%s : %v", name, err)
}
}
}
}
return nil
}
下面的Unpack()
函数做了三件事情:
首先,调用
req.ParseForm()
来解析请求。在这之后,req.Form
就有了所有的请求参数,这个方法对HTTP
的GET
和POST
请求都适用。然后,
Unpack()
函数构造了一个从每个有效字段名到对应字段变量的映射。在字段有标签时,有效字段名与实际字段名可能会有差别。reflect.Type
的Field()
方法会返回一个reflect.StructField
类型,这个类型提供了每个字段的名称、类型以及一个可选的标签。它的Tag
字段类型为reflect.StructTag
,底层类型为字符串,提供了一个Get()
方法用于解析和提取对于一个特定键的子串。最后,
Unpack()
遍历HTTP
参数中的键值对,并且更新对应的结构体字段。注意,同一个参数可能会出现多次。如果有这种情况并且字段是slice
类型,则这个参数的所有值都会追加到slice
里。如果不是,则这个字段会被多次覆盖,仅有最后一个值才是有效的。
populate()
函数负责从单个HTTP请求参数值填充单个字段v(或者slice字段中的每个元素)。现在,它仅支持字符串、有符号整数和布尔值。对于其他类型的支持,可以自己练习。
func populate(v reflect.Value, value string) error {
switch v.Kind() {
case reflect.String:
v.SetString(value)
case reflect.Int:
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
v.SetInt(i)
case reflect.Bool:
b, err := strconv.ParseBool(value)
if err != nil {
return err
}
v.SetBool(b)
default:
return fmt.Errorf("unsupported kind %s", v.Type())
}
return nil
}
显示类型的方法
使用reflect.Type
来显示一个任意值的类型并枚举它的方法:
func Print(x interface{}) {
v := reflect.ValueOf(x)
t := v.Type()
fmt.Printf("type %s\n", t)
for i := 0; i < v.NumMethod(); i++ {
methodType := v.Method(i).Type()
fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name, strings.TrimPrefix(methodType.String(), "func"))
}
}
reflect.Type
和reflect.Value
都有一个叫做Method()
的方法,每个t.Method(i)
都会返回一个描述了这个方法的名称和类型的reflect.Method
类型实例。而每个v.Method()
都会返回一个已绑定接收者方法的reflect.Method
类型实例。
使用reflect.Value.Call()
方法可以调用Func
类型的Value,但本示例程序只需要它的类型。
下面就是两个类型time.Duration和*strings.Replacer的方法列表:
Print(time.Hour)
// type time.Duration
// func (time.Duration) Hours() float64
// func (time.Duration) Microseconds() int64
// func (time.Duration) Milliseconds() int64
// func (time.Duration) Minutes() float64
// func (time.Duration) Nanoseconds() int64
// func (time.Duration) Round(time.Duration) time.Duration
// func (time.Duration) Seconds() float64
// func (time.Duration) String() string
// func (time.Duration) Truncate(time.Duration) time.Duration
Print(new(strings.Replacer))
// type *strings.Replacer
// func (*strings.Replacer) Replace(string) string
// func (*strings.Replacer) WriteString(io.Writer, string) (int, error)
推荐阅读